P13.1: final vibe integration — playthrough validation, full golden sweep, finalized docs
Integration-only (no logic changes). Validated the full candy vibe in the booted iPhone sim and brought every artifact in line with the shipped candy palette + Triple Treat SFX bank. Goldens — swept all 23: - Refreshed 15 that predated the candy palette (P12): p6_idle_t0, p6_idle_mid, p6_select, p7_restart, p5_swap_before/after, p6_anim_swap/clear/fall/after, p6_fx, p6_fx_after, p6_fx_match, p6_inputlock_board, p11_combo_deep — re-captured via the documented M3TE_* hooks. - Left 5 unchanged (board+HUD region byte-identical to the current build, verified by cropped-region hash): p4_board, p4_hud, p9_polish, p7_win, p7_lose. - Removed 3 obsolete pre-board orange-quad goldens (app no longer renders them): p0_quad, p0_input_before, p0_input_after. Docs — README.md: - Section 2 now describes the candy board (not the old orange quad) and points at goldens/p6_idle_t0.png; dropped the removed p0_* references. - Added the final audio model: Triple Treat SFX provenance + per-cue mapping, the per-round ascending cascade (one combo cue per round, clamped at combo5), the WAVE/mono/44100/Int16 @ -15 dBFS format spec, and the cue-log capture commands. - Added image-art asset regeneration (codex imagegen via codex exec + sips normalize to exact per-asset dims/format). Gate: ios-sim build links (exit 0); 21/21 pure-sx logic tests pass. Playthrough evidence (cue NSLog ascending combo1..combo5 + win/lose stingers, screenshots) captured in the worker report.
133
README.md
@@ -32,7 +32,9 @@ bash tools/run_tests.sh
|
|||||||
### 2. iOS Simulator build + launch
|
### 2. iOS Simulator build + launch
|
||||||
|
|
||||||
Build the app for the simulator, then install/launch it on an available device
|
Build the app for the simulator, then install/launch it on an available device
|
||||||
and screenshot the rendered scene (blue background + a centered orange quad).
|
and screenshot the rendered scene — the brighter **candy** match-3 board: a soft
|
||||||
|
pink→lavender→blue gradient background, an 8×8 grid of glossy candy gems on cream
|
||||||
|
cell tiles, and a grape candy HUD card (score / moves) above the grid.
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# Build the .app bundle (sx-out/ios/M3te.app):
|
# Build the .app bundle (sx-out/ios/M3te.app):
|
||||||
@@ -53,10 +55,16 @@ xcrun simctl launch booted co.swipelab.m3te
|
|||||||
xcrun simctl io booted screenshot /tmp/m3te.png
|
xcrun simctl io booted screenshot /tmp/m3te.png
|
||||||
```
|
```
|
||||||
|
|
||||||
The screenshot should match `goldens/p0_quad.png` (a centered orange quad over a
|
The screenshot should match `goldens/p6_idle_t0.png` (the resting candy board),
|
||||||
blue clear), modulo the status-bar clock — pixel-exact equality is not required.
|
modulo the status-bar clock — pixel-exact equality is not required; compare the
|
||||||
A tap on the quad flips its color (orange ↔ green); see
|
board + HUD region, not the top status strip. A tap selects a cell; a swipe
|
||||||
`goldens/p0_input_before.png` / `goldens/p0_input_after.png`.
|
between two adjacent gems commits a swap (legal swaps cascade + score, illegal
|
||||||
|
ones ping back). The deterministic capture hooks below pin a chosen scene so each
|
||||||
|
golden reproduces from a clean checkout.
|
||||||
|
|
||||||
|
> The earlier `p0_*` goldens (a pre-board orange quad over a blue clear, from the
|
||||||
|
> P0 foundation slice) were removed in P13.1: the app no longer renders that
|
||||||
|
> scene, so they could only ever mislead.
|
||||||
|
|
||||||
### Deterministic animation capture (P6.3)
|
### Deterministic animation capture (P6.3)
|
||||||
|
|
||||||
@@ -197,3 +205,118 @@ env SIMCTL_CHILD_M3TE_ANIM_TIME=0 SIMCTL_CHILD_M3TE_SELECT=27 \
|
|||||||
|
|
||||||
The selection gloss is purely visual: it never gates input (`BoardAnim.active`
|
The selection gloss is purely visual: it never gates input (`BoardAnim.active`
|
||||||
owns gating) and never touches board / score / move state.
|
owns gating) and never touches board / score / move state.
|
||||||
|
|
||||||
|
## Audio bank (P10) — final model
|
||||||
|
|
||||||
|
The SFX bank (`audio.sx`) is a purely additive layer over iOS **System Sound
|
||||||
|
Services** (AudioToolbox, reached by `#foreign` FFI exactly as the platform reaches
|
||||||
|
UIKit/Metal). Each cue is loaded once at startup into its own `SystemSoundID`;
|
||||||
|
playback is a single C call. The bank only plays when *every* cue loaded (`loaded`
|
||||||
|
gate), so a partial/failed bank mutes rather than playing a stale id. It never
|
||||||
|
reads or mutates score / board / move state — `board_view` and the frame loop just
|
||||||
|
tell it an event happened.
|
||||||
|
|
||||||
|
### Provenance — the user-provided "Triple Treat SFX" pack
|
||||||
|
|
||||||
|
The nine shipped cues are best-fit selections from the user-supplied **Triple Treat
|
||||||
|
SFX** pack (`Triple_Treat_SFX.zip`, ~30 MB, 280 Unity-style files). The pack itself
|
||||||
|
is **not committed** — only the selected, converted cues live in `assets/audio/`;
|
||||||
|
the source archive is kept outside the repo (in `~/Downloads`). Full per-cue notes
|
||||||
|
are in `assets/audio/LICENSE.txt`. The shipped game never re-synthesizes anything;
|
||||||
|
it only loads the finished WAVs.
|
||||||
|
|
||||||
|
| cue | game event | pack source (under `Triple Treat SFX/`) |
|
||||||
|
|--------------|------------------------------------|------------------------------------------|
|
||||||
|
| `swap.wav` | every committed swipe (subtle) | `Transition SFX/Swipe FX 1-RCM.wav` |
|
||||||
|
| `match.wav` | first clear of a legal move (pop) | `Pop:Bubble SFX/Pop FX 5-RCM.wav` |
|
||||||
|
| `combo1.wav` | cascade round 1 (dullest) | `Match SFX/Match FX 2-RCM.wav` (~1.7 kHz)|
|
||||||
|
| `combo2.wav` | cascade round 2 | `Match SFX/Match FX 4-RCM.wav` (~2.1 kHz)|
|
||||||
|
| `combo3.wav` | cascade round 3 | `Match SFX/Match FX 6-RCM.wav` (~3.2 kHz)|
|
||||||
|
| `combo4.wav` | cascade round 4 | `Match SFX/Match FX 7-RCM.wav` (~4.7 kHz)|
|
||||||
|
| `combo5.wav` | cascade round ≥5 (brightest) | `Match SFX/Match FX 3-RCM.wav` (~6.8 kHz)|
|
||||||
|
| `win.wav` | level won (stinger) | `Success:Power-Up SFX/Power Up FX 1-RCM.wav` |
|
||||||
|
| `lose.wav` | level lost (stinger) | `Fail SFX/Fail FX 2-RCM.wav` |
|
||||||
|
|
||||||
|
The Match-FX set does not cleanly pitch-ascend, so `combo1..5` are ordered by
|
||||||
|
ascending **spectral brightness** (centroid 1.68 < 2.09 < 3.18 < 4.70 < 6.77 kHz)
|
||||||
|
so a deeper cascade reads as more exciting.
|
||||||
|
|
||||||
|
### Per-round ascending cascade
|
||||||
|
|
||||||
|
The cascade plays **one ascending cue per cascade round** — round 1 → `combo1`,
|
||||||
|
round 2 → `combo2`, … clamped at `combo5` — *not* a single end-of-move sound. Each
|
||||||
|
cue fires on the move's animation timeline, edge-triggered as that round's clear
|
||||||
|
begins: the frame loop (`main.sx`) diffs `cascade_rounds_started(elapsed, rounds)`
|
||||||
|
against `BoardAnim.cascade_fired` and plays the next combo cue per newly-cleared
|
||||||
|
round (`sfx_cascade`). The depth→index clamp (`cascade_cue_index`: depth ≤ 1 → 0,
|
||||||
|
depth ≥ 5 → 4) is pure + headless, snapshot-tested by `tests/cascade_cue.sx` and
|
||||||
|
`tests/cascade_rounds.sx`. A single match (one round) plays only the `match` pop —
|
||||||
|
no combo cue. The `swap` cue plays for any committed gesture (legal or the reverted
|
||||||
|
ping-back); a legal move adds the `match` pop on its first clearing round; the
|
||||||
|
`win`/`lose` stinger fires once, edge-triggered, as the banner comes up.
|
||||||
|
|
||||||
|
### Format / level spec
|
||||||
|
|
||||||
|
Every cue is delivered in exactly the form System Sound Services loads directly:
|
||||||
|
**WAVE / mono / 44100 Hz / signed-16-bit PCM**. The pack sources are 24-bit /
|
||||||
|
48 kHz / stereo; each was down-mixed to mono, trimmed to its punchy window, eased
|
||||||
|
in with a short fade, rounded out with a cosine fade-out tail, and **peak-normalized
|
||||||
|
to a gentle −15 dBFS** (the user rejected aggressive SFX twice). The candy character
|
||||||
|
of the pack is preserved — the cues are not re-synthesized.
|
||||||
|
|
||||||
|
### Capturing the cue ordering
|
||||||
|
|
||||||
|
Every play is `NSLog`'d, so a playthrough's cue order is readable from the device
|
||||||
|
log. The deep cascade (`M3TE_FX=11`, depth-5 on seed 1337) must be launched **live**
|
||||||
|
(no `M3TE_ANIM_TIME` pin) so the timeline advances and the per-round cues fire in
|
||||||
|
sequence:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Deep cascade, live — fires combo1..combo5 one per round on the timeline:
|
||||||
|
xcrun simctl terminate booted co.swipelab.m3te
|
||||||
|
SIMCTL_CHILD_M3TE_FX=11 xcrun simctl launch booted co.swipelab.m3te
|
||||||
|
xcrun simctl spawn booted log show --last 20s \
|
||||||
|
--predicate 'eventMessage CONTAINS "[sx] audio"' --style compact
|
||||||
|
# Win / lose stingers (also live so the banner edge-triggers the cue):
|
||||||
|
SIMCTL_CHILD_M3TE_TARGET=0 xcrun simctl launch booted co.swipelab.m3te # cue win
|
||||||
|
SIMCTL_CHILD_M3TE_MOVE_LIMIT=0 xcrun simctl launch booted co.swipelab.m3te # cue lose
|
||||||
|
```
|
||||||
|
|
||||||
|
## Asset regeneration
|
||||||
|
|
||||||
|
The shipped game only loads finished assets; the steps below rebuild them and are
|
||||||
|
never run at play time. Refresh any affected `goldens/` after an art change.
|
||||||
|
|
||||||
|
### Audio (the SFX bank)
|
||||||
|
|
||||||
|
The shipped cues are the Triple Treat selections above, converted to the canonical
|
||||||
|
container with `afconvert` (WAVE / `LEI16` / 44100 / mono) and verified with
|
||||||
|
`tools/measure_pitch.py` — the combo cues' spectral centroids must ascend
|
||||||
|
monotonically (and beat `clear.wav`'s ~784 Hz CC0 baseline reference clip).
|
||||||
|
`tools/synth_audio.py` is the build-time DSP path (down-mix, trim, fade, peak-
|
||||||
|
normalize, optional resample). The 30 MB pack and its `.meta` / `__MACOSX` cruft
|
||||||
|
are not committed.
|
||||||
|
|
||||||
|
### Image art
|
||||||
|
|
||||||
|
Real art is produced with codex's built-in **`imagegen`** tool through `codex exec`,
|
||||||
|
then `sips`-normalized to each asset's exact dimensions and source format:
|
||||||
|
|
||||||
|
| asset | dims (px) | format | role |
|
||||||
|
|-----------------------------|-----------|--------|-----------------------------------|
|
||||||
|
| `assets/board/background.png` | 863×1822 | PNG | full-view candy gradient backdrop |
|
||||||
|
| `assets/board/cell.png` | 128×128 | PNG | one grid cell tile |
|
||||||
|
| `assets/gems/gems.png` | 768×128 | PNG | 6 gem columns (a gem's UV column = its index) |
|
||||||
|
| `assets/fx/particle.png` | 256×256 | PNG | soft match-burst sprite (tinted per gem) |
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 1. Generate (codex's imagegen tool, driven non-interactively):
|
||||||
|
codex exec "use the imagegen tool to render <prompt for the asset>"
|
||||||
|
# 2. Normalize to the exact per-asset dims + format, e.g. the gem sheet:
|
||||||
|
sips -z 128 768 --setProperty format png <generated>.png --out assets/gems/gems.png
|
||||||
|
```
|
||||||
|
|
||||||
|
After any art change, re-capture the affected goldens with the deterministic hooks
|
||||||
|
above (`M3TE_ANIM_TIME` / `M3TE_SELECT` / `M3TE_FX` / `M3TE_TARGET` /
|
||||||
|
`M3TE_MOVE_LIMIT` / `M3TE_RESTART`) and state per golden whether it was refreshed,
|
||||||
|
left unchanged, or removed.
|
||||||
|
|||||||
|
Before Width: | Height: | Size: 75 KiB |
|
Before Width: | Height: | Size: 75 KiB |
|
Before Width: | Height: | Size: 74 KiB |
|
Before Width: | Height: | Size: 3.7 MiB After Width: | Height: | Size: 3.3 MiB |
|
Before Width: | Height: | Size: 3.8 MiB After Width: | Height: | Size: 3.2 MiB |
|
Before Width: | Height: | Size: 3.8 MiB After Width: | Height: | Size: 3.2 MiB |
|
Before Width: | Height: | Size: 3.8 MiB After Width: | Height: | Size: 3.2 MiB |
|
Before Width: | Height: | Size: 3.7 MiB After Width: | Height: | Size: 3.2 MiB |
|
Before Width: | Height: | Size: 3.9 MiB After Width: | Height: | Size: 3.3 MiB |
|
Before Width: | Height: | Size: 3.8 MiB After Width: | Height: | Size: 3.2 MiB |
|
Before Width: | Height: | Size: 3.9 MiB After Width: | Height: | Size: 3.2 MiB |
|
Before Width: | Height: | Size: 3.7 MiB After Width: | Height: | Size: 3.2 MiB |
|
Before Width: | Height: | Size: 3.7 MiB After Width: | Height: | Size: 3.2 MiB |
|
Before Width: | Height: | Size: 3.6 MiB After Width: | Height: | Size: 3.2 MiB |
|
Before Width: | Height: | Size: 3.5 MiB After Width: | Height: | Size: 3.2 MiB |
|
Before Width: | Height: | Size: 3.8 MiB After Width: | Height: | Size: 3.2 MiB |
|
Before Width: | Height: | Size: 3.7 MiB After Width: | Height: | Size: 3.2 MiB |
|
Before Width: | Height: | Size: 3.6 MiB After Width: | Height: | Size: 3.2 MiB |