Play Instigator

Blog

Dev progress on the left. Personal thoughts on the right.

Dev Log

AI-generated · automated

I'm a solo dev working part-time. No time for proper devlogs, so I automated them. Every week, a local AI reads my raw session notes and writes a summary. No editorial polish — just proof the game is alive.

Musings

Thoughts on making this game, running a studio alone, and whatever else comes up.

Hey there! This week in Ritual & Ruin dev land, we've been diving into some cool updates that should make your game experience even more vibrant and smooth.

First off, I tackled a pesky issue with the Hazard Cube material. It was showing up as magenta due to an old Unity shader mismatch, but now it's sporting a new dedicated material in Assets/ProtoV2/Materials. This means the cube looks just how you'd expect it to, without any strange colors popping up unexpectedly.

Next, we've made some exciting progress on our Color Palette Workflow. We wrapped up Phase 4 and wired everything into MatchScene. Now, changing color palettes is a breeze—a quick dropdown selection takes less than 30 seconds! This will make customizing the game's look faster and more intuitive for everyone. I've set up a new system where you can easily preview and apply different color schemes with minimal fuss.

On the to-do list, we're planning to generate some additional palettes so you'll have even more options to choose from. Plus, if future updates show that certain elements need more attention for readability, like wall or gap-edge colors, we’ll make sure they’re up to par too.

Keep an eye out for these changes, and thanks for sticking with us on this colorful journey!

Raw session notes

2026-05-07 — Color Palette Phase 4 wiring + Hazard material fix

Hazard pink material — fix

Root cause: Hazard/Cube MeshRenderer was referencing Assets/Obi/Samples/Common/SampleResources/Materials/FinishLine.mat, an Obi sample asset serialized in Unity 2019.3 against the Built-in Standard shader. URP can't render Standard → magenta. Almost certainly re-imported by the Obi 7.1.1 upgrade on 2026-04-29 (the prior MaterialShaderFixer URP patch from 2026-03-28 didn't survive the package update). Project rule says don't modify Assets/Obi/, so the fix is a dedicated material under Assets/ProtoV2/Materials/.

Fix:

  • New material: Assets/ProtoV2/Materials/M_Hazard.mat — URP/Unlit, full transparent recipe (_Surface=1, _SrcBlend=5, _DstBlend=10, _ZWrite=0, _Cull=0, _SURFACE_TYPE_TRANSPARENT keyword, RenderType: Transparent, m_CustomRenderQueue=3000), tint _BaseColor (1, 0.18, 0.18, 0.35).
  • Assigned to Hazard/Cube MeshRenderer.sharedMaterial via Unity MCP.
  • Note: MCP manage_material set_material_shader_property reported success but didn't persist values to the .mat file — patched the YAML directly via Write to apply the transparent recipe in one shot.

Verification: Unity console clean (0 errors, 0 warnings); cube renderer's sharedMaterial confirmed = Assets/ProtoV2/Materials/M_Hazard.mat.

Color Palette Workflow Phase 4 — end-to-end

Phase 4 scripts shipped 2026-05-06; today wired them into MatchScene. Phase 4 is the bottleneck-killer for the [Color Composition Workflow](../../Games/Ritual%20&%20Ruin/Confirmed/design/Color%20Composition%20Workflow.md) — turns a 5-min-per-palette material wrangle into a ~30-second dropdown swap.

What changed

Scripts (Assets/ProtoV2/Scripts/):

  • ColorSystem/ColorPaletteSO.cs[CreateAssetMenu] SO with floor / wall / background + altarAccent / gapEdgeAccent + optional bloodTintAccent (toggle). paletteName + description for the dropdown.
  • ColorSystem/PaletteApplier.cs[ExecuteAlways] [DisallowMultipleComponent] MonoBehaviour with material-array slots per palette role. Pushes _BaseColor (URP) and _Color (legacy) on each wired material. Optional camera clear-color binding. OnValidate auto-applies via EditorApplication.delayCall.
  • Editor/ColorPalettePreviewWindow.csWindow → Ritual & Ruin → Color Palette Preview window: palette dropdown, swatch preview, Push-Live / Capture / Cycle-All. Captures land in PaletteCaptures/ at the project root (outside Assets/ so the AssetDatabase doesn't ingest them).

Data:

  • New folders: Assets/ProtoV2/Data/ and Assets/ProtoV2/Data/ColorPalettes/.
  • Assets/ProtoV2/Data/ColorPalettes/Default.asset — first ColorPaletteSO, baseline / clinical mood.

Scene (Assets/ProtoV2/Scenes/MatchScene.unity):

  • New GameObject: ColorPaletteRoot (root, world origin) with PaletteApplier component.
  • Wiring:
  • _activePaletteDefault.asset
  • _floorMaterials → [ChunkFloorTile.mat, ChunkFloorTile_Boundary.mat]
  • _backgroundClearCameraMain Camera
  • Scene saved.

Why these scope choices

  • Walls, gap edges, altar accent NOT wired: each one creates its material at runtime from [SerializeField] Color fields (BoundaryWalls.cs, GapHighlighter.cs, AltarFillVisual.cs) — no .mat asset exists to slot in. Making them palette-driven needs script changes (expose color setters on each, have PaletteApplier push palette colors into them). Deferred — Phase 5 readability validation will tell us which ones are worth the work.
  • Camera clear flags left as Skybox: switching to Solid Color is a visible scene change beyond pure wiring. The camera reference is wired so that flipping Clear Flags → Solid Color in the Inspector immediately makes backgroundColor palette-driven.
  • Materials chosen — ChunkFloorTile.mat and ChunkFloorTile_Boundary.mat: these are the two real .mat assets used by the chunk prefab and floor boundary, dedicated to the floor surface, no shared use elsewhere. Safe to recolor.

Verification

  • Unity console clean (0 errors, 0 warnings) after script add, scene save.
  • PaletteApplier component state verified via MCP resource: _activePalette, _floorMaterials array, _backgroundClearCamera all serialized correctly.

Files modified

  • New: Assets/ProtoV2/Scripts/ColorSystem/ColorPaletteSO.cs
  • New: Assets/ProtoV2/Scripts/ColorSystem/PaletteApplier.cs
  • New: Assets/ProtoV2/Scripts/Editor/ColorPalettePreviewWindow.cs
  • New: Assets/ProtoV2/Materials/M_Hazard.mat
  • New: Assets/ProtoV2/Data/ColorPalettes/Default.asset
  • Modified: Assets/ProtoV2/Scenes/MatchScene.unity — added ColorPaletteRoot GameObject + reassigned Hazard/Cube material

Follow-ups

  • (User) Open Window → Ritual & Ruin → Color Palette Preview and confirm Default appears in the dropdown; click "Push palette live" and watch ChunkFloorTile.mat _BaseColor update in the Scene view.
  • (User) Optional: flip Main Camera Clear Flags to Solid Color when ready to test palette-driven backgrounds.
  • Phase 3 candidate generation — produce 6–10 named palettes as additional ColorPaletteSO assets under the same folder. Each becomes a dropdown entry automatically.
  • If Phase 5 validation shows wall / gap-edge color is load-bearing for readability, build the deferred runtime-color setter pattern on BoundaryWalls / GapHighlighter / AltarFillVisual.

Certainly! Below is a structured summary of the provided technical update and actions related to upgrading Obi Fluid 7.1.0 to 7.1.1, along with addressing issues and verification steps:

Upgrade Summary

What Changed: Obi Fluid Version: Upgraded from version 7.1.0 to 7.1.1 on April 22, 2026. Workarounds Removed: ObiLeakWorkaround.cs and its corresponding .meta file were deleted as the upgrade resolved a memory leak issue. Scene Modifications: In MatchScene.unity, removed the ObiLeakWorkaround MonoBehaviour reference from the ObiSolver GameObject’s component list.

Documentation Updates: Updated MEMORY.md to reflect the version change and deprecated previous leak-fix entries, redirecting them to new documentation. A pre-existing document (docs/OBI_UPGRADE_7_1_1.md) was confirmed accurate post-upgrade.

Reasons for Changes

The upgrade primarily addressed two key issues: Memory Leak: Version 7.1.1 fixed a memory leak in ObiFluidRendererFeature, which was the exact issue previously patched by ObiLeakWorkaround.cs. Collider World Issue: Fixed an unrelated problem with particle reactivity to colliders that did not affect the current implementation.

Verification Process

Static Code Analysis: Conducted a grep search for references to ObiLeakWorkaround and its GUID, confirming no residual code existed. Ensured all Obi types/methods in use were compatible with version 7.1.1.

Compilation Check: Verified zero errors and seven warnings during live compilation within the Unity Editor. The warnings pertained to deprecated methods used by Obi’s sample scripts, not user code.

Batch-mode Compile Attempt: Faced a failure due to project lock but relied on successful live editor compilation as verification of no introduced issues.

Files Modified

Deleted: ObiLeakWorkaround.cs and its .meta file. Modified: MatchScene.unity: Removed obsolete component reference related to the workaround. Documentation Updated: Added entry in MEMORY.md. Verified existing upgrade documentation (docs/OBI_UPGRADE_7_1_1.md).

Follow-Up Actions

Conduct a play session of MatchScene for at least 5 minutes to ensure that fluid rendering functions correctly and native memory usage remains stable. Consider potential performance improvements unlocked by the new version, such as utilizing solver boundary limits or optimizing blood attribution tracking.

This summary captures the essence of the technical changes, rationale, verification efforts, and next steps following the Obi Fluid upgrade.

Raw session notes

2026-04-27 — Lobby Mouse Cursor Dies on Gamepad Connect (Missing EventSystem)

Symptom

In LobbyScene (rebinder UI), the mouse cursor stopped dispatching click/hover events to the UI Toolkit panel the moment a gamepad was connected. Disconnecting the gamepad restored mouse dispatch. Reproducible regardless of whether any player was set to Controller mode. MainMenu and MatchScene were unaffected — mouse worked fine there even with multiple gamepads connected and characters being driven by them.

A workaround was already shipped at InputRebindUIController.cs:2870-2891 (gamepad Start button / Enter key advances scene without needing the mouse). The actual cause remained unidentified until this session.

Investigation that led nowhere

Three failed hypotheses, each ruled out via runtime diagnostics dumping InputUser.all state:

1. Player-InputUser scheme filter (Option E test) — commented out user.ActivateControlScheme("Gamepad") in PlayerSetup.PairWithGamepad and ReapplyDevicePairing. Mouse still broken on gamepad-connect. Diagnostic confirmed Player 1's user stayed on 'Keyboard&Mouse' scheme — never flipped to Gamepad — so scheme-based device filtering wasn't the cause.

2. Mouse paired to player blocking UI — diagnostic showed paired=[Keyboard,Xbox Controller] for Player 1; no Mouse in any user's paired-device list. ReleaseMouseToUI() was already working correctly. Mouse was globally free.

3. Legacy input fallback dying on gamepad-connect — checked ProjectSettings/ProjectSettings.asset; activeInputHandler: 1 (New Input System only, no legacy). No fallback to die.

Root cause

UnityEngine.EventSystems.EventSystem.current was null throughout the session — no EventSystem GameObject existed in LobbyScene.unity. With no explicit EventSystem present, UI Toolkit's runtime panel auto-creates an internal DefaultEventSystem (a different class from UnityEngine.EventSystems.EventSystem) to dispatch input. That DefaultEventSystem has a documented gamepad-takeover behavior: when a gamepad is connected, it switches to gamepad-navigation mode and suppresses pointer-event dispatch. So the cursor dies the moment a gamepad is detected, regardless of pairing.

Confirmed by grepping all scenes:

| Scene | EventSystem GO | Mouse + gamepad |

|---|---|---|

| MainMenu.unity | yes (line 134) | works |

| MatchScene.unity | yes (line 4484) | works (dev panel clickable while character is on a gamepad) |

| LobbyScene.unity | missing | broken |

| ExperimentBriefScene.unity | missing | not tested — assumed broken |

| ObjectiveBriefScene.unity | missing | not tested — assumed broken |

The earlier StripGamepadNavigationFromUIModule() in InputBindingPersistenceManager.cs:107-118 (which nulls out move/submit/cancel on the EventSystem's UI module after every scene load) had been a no-op in these three scenes — it short-circuits when EventSystem.current == null.

Fix

Added an EventSystem GameObject to each of the three scenes lacking one. Each has:

  • UnityEngine.EventSystems.EventSystem (defaults: sendNavigationEvents=true, dragThreshold=10)
  • UnityEngine.InputSystem.UI.InputSystemUIInputModule with actionsAsset wired to Assets/InputSystem_Actions.inputactions (GUID ca9f5fa95ffab41fb9a615ab714db018) — same wiring as MatchScene's existing EventSystem at MatchScene.unity:4490-4520.

Files modified (Unity scenes):

  • Assets/ProtoV2/Scenes/LobbyScene.unity — EventSystem added
  • Assets/ProtoV2/Scenes/ExperimentBriefScene.unity — EventSystem added
  • Assets/ProtoV2/Scenes/ObjectiveBriefScene.unity — EventSystem added

With an explicit EventSystem present, UI Toolkit stops creating its internal DefaultEventSystem and routes pointer events through ours instead — and the existing StripGamepadNavigationFromUIModule() now actually runs, nulling out gamepad-nav action references on the lobby UI module so the panel only responds to mouse.

Diagnostic instrumentation added during the investigation was reverted in the same change:

  • PlayerSetup.cs:477, 523 — restored user.ActivateControlScheme("Gamepad") calls
  • PlayerSetup.cs — deleted DumpInputState method
  • InputRebindUIController.cs:121 — deleted _diagNextDumpTime field
  • InputRebindUIController.cs Update — deleted throttled [INPUT_DIAG/Update] log block; kept the gamepad-Start / Enter-key fallback as a safety net (low cost, useful if mouse ever regresses)

Compile: 0 errors, 0 warnings.

Verification

  • Pre-fix runtime diagnostic confirmed EventSystem.current=null in LobbyScene while mouse was working (pre-gamepad), establishing UI Toolkit was using its DefaultEventSystem — and confirmed mouse.pos continued updating post-gamepad-connect (so the bug was at the dispatch layer, not the device layer).
  • Post-fix scene grep confirms all three target scenes now contain an EventSystem MonoBehaviour entry.
  • In-Editor playtest confirmed working ✅ — user verified mouse cursor responds to clicks in LobbyScene rebinder UI with one or more gamepads connected. Brief scenes (ExperimentBriefScene, ObjectiveBriefScene) not interactively tested but should follow the same path.

Follow-ups

  • TODO: Verify in Play Mode that mouse cursor responds to clicks in LobbyScene rebinder UI when one or more gamepads are connected. Then repeat for ExperimentBriefScene and ObjectiveBriefScene if either has interactive UI.
  • TODO (optional): Once verified, consider removing the gamepad-Start / Enter workaround in InputRebindUIController.Update() since the underlying bug is fixed. Or keep it as a deliberate redundancy — the cost is negligible and it covers any future scene that ships without an EventSystem.
  • Convention to enforce going forward: any new scene that hosts a UIDocument / UI Toolkit panel must include an EventSystem GameObject with an InputSystemUIInputModule. Without it, the scene will silently work without gamepads attached and break the moment any user connects one. Worth adding to the project setup wizard or a scene-validation editor script eventually.
  • No impact on MatchScene gameplay. Player input routing (gamepad-driven character control) is independent of UI dispatch and unchanged.

2026-04-29 — All technical docs migrated to PlayInstigator Docs

Decision

All documentation now lives in PlayInstigator Docs. The Unity project's docs/ folder has been removed. New rule: every doc — technical, design, content — goes to playinstigator_docs/Games/Ritual & Ruin/. There is no docs/ folder in the project repo anymore.

What moved

7 files from E:/Unity/Projects/PrototypeV2/docs/playinstigator_docs/Games/Ritual & Ruin/Confirmed/tech/ (renamed to Title Case to match existing tech folder convention):

| Old | New |

|---|---|

| docs/SYSTEMS.md | Confirmed/tech/Core Systems Reference.md |

| docs/SETTINGS.md | Confirmed/tech/Settings System Reference.md |

| docs/INPUT.md | Confirmed/tech/Input System Reference.md |

| docs/WORKFLOW.md | Confirmed/tech/Workflow and Conventions.md |

| docs/UI_PIXEL_PERFECT.md | Confirmed/tech/UI Pixel-Perfect Canvas.md |

| docs/OBI_UPGRADE_7_1_1.md | Confirmed/tech/Obi Fluid 7.1.1 Upgrade.md |

| docs/CONTROLLER_PAIRING_ANALYSIS.md | Confirmed/tech/Controller Pairing Analysis.md |

Cross-references updated

  • CLAUDE.md (project) — Detailed References table rewritten with new paths; "Where to create new documents" rule changed to require ALL docs in PlayInstigator Docs; inline rule mentions for Settings (rule 6), Workflow (rule 8), Pixel-Perfect (rule 9) all repointed.
  • Assets/ProtoV2/Scripts/UI/UIBuilder.cs:34 — header comment repointed.
  • Assets/ProtoV2/Scripts/MainMenuController.cs:62 — comment repointed.
  • MEMORY.md:12 — OBI_UPGRADE link repointed.
  • Confirmed/tech/UI Pixel-Perfect Canvas.md — sibling cross-ref to SYSTEMS.md normalized to sibling form.
  • Confirmed/tech/UI Prefab Structure.md — pre-existing stale ref to docs/UI_PIXEL_PERFECT.md fixed to sibling form.

Cleanup

  • E:/Unity/Projects/PrototypeV2/docs/ deleted.
  • No orphaned docs.meta (folder wasn't tracked as Unity asset).

Not touched

  • .claude/settings.local.json — auto-generated permission allowlist contains historical command paths; not load-bearing for behaviour.
  • .omc/plans/input-rebinder-uitk-restyle.md — archived planning artifact from a prior session; references the old doc paths but is not consulted by current workflows.

Verification

  • mcp__mcp-for-unity__refresh_unity with compile=request — clean, zero errors. Source-file changes were comment-only.
  • Grep for docs/SYSTEMS|docs/SETTINGS|docs/INPUT|docs/WORKFLOW|docs/UI_PIXEL_PERFECT|docs/OBI_UPGRADE|docs/CONTROLLER_PAIRING across project + memory: only the two non-load-bearing locations above remain.

Why this matters for future sessions

  • Searching for technical docs: look in playinstigator_docs/Games/Ritual & Ruin/Confirmed/tech/ first.
  • Creating a new technical doc: drop it in Confirmed/tech/ with Title Case naming. Don't create a docs/ folder in the project repo.
  • The CLAUDE.md "Where to create new documents" table now lists all sub-destinations under PlayInstigator Docs.

---

Obi Fluid 7.1.0 → 7.1.1 Upgrade

What changed

  • Asset: Virtual Method — Obi Fluid (Asset Store id 63067), 7.1.0 → 7.1.1 (released 2026-04-22). User imported via Package Manager.
  • Assets/ProtoV2/Scripts/BloodSystem/ObiLeakWorkaround.cs deleted (script + .meta).
  • MatchScene.unity: stripped the ObiLeakWorkaround MonoBehaviour (fileID 1820732523) and its entry in the ObiSolver GameObject's m_Component list (around line 8285). The ObiSolver GameObject (&1820732520) now has only Transform + ObiSolver components — no other state altered.
  • MEMORY.md (auto-memory index): added 2026-04-29 entry recording the upgrade and superseded leak-fix entry from 2026-03-29 with a pointer to the new state.
  • docs/OBI_UPGRADE_7_1_1.md (already authored 2026-04-28 in pre-upgrade analysis pass) — content remains accurate.

Why

Obi 7.1.1's official changelog (Assets/Obi/CHANGELOG_fluid.txt) reads:

Fix #1 is the exact bug ObiLeakWorkaround.cs patched around (originally documented in MEMORY.mdproject_obi_memory_fix.md, dev log 2026-03-29 D3D12 pool leak). Symptom matches exactly: many instances of Hidden/AccumulateTransmissionURP, Hidden/IndirectSurfaceURP, Hidden/IndirectThicknessURP materials accumulating over time, originating from ObiFluidRendererFeature. With the upstream fix the workaround is dead code — keeping it would just waste a Resources.UnloadUnusedAssets() GC pass every 30s for nothing.

Fix #2 (ObiColliderWorld re-instantiate-in-same-frame) doesn't observably affect us — the floor system pools chunks (FloorGenerator.GetChunkFromPool/ReturnChunkToPool) and Floor.DestroyFloor → next-floor GenerateFloor happen on different objects across frames, not "same prefab destroyed and re-instantiated in the same frame." Free safety net, no action needed.

Verification

  • Static: grep ObiLeakWorkaround Assets/ → 0 hits. grep 3e7b0f80e232b5a429de5e3e21fc2c3e Assets/ (script GUID) → 0 hits. grep "m_Script: {fileID: 0, guid: 00000000..." Assets/ProtoV2/ → 0 hits (no orphaned script refs).
  • API surface compatibility: every Obi type/method our code uses verified to exist in 7.1.1 with matching signatures —
  • ObiSolver.OnCollision event (Common/Solver/ObiSolver.cs:116)
  • ObiNativeContactList, solver.simplexCounts.GetSimplexStartAndSize(int, out int) (Common/DataStructures/SimplexCounts.cs:25)
  • solver.simplices, colors, positions, velocities, particleToActor (ObiSolver.cs:481)
  • Oni.Contact struct with bodyA, bodyB, distance (Oni.cs:234)
  • ObiColliderWorld.GetInstance(), colliderHandles[].owner (Common/Collisions/ObiColliderWorld.cs:98, 64)
  • ObiCollider.Thickness
  • ObiEmitter.speed, KillParticle(int) (Fluid/Actors/ObiEmitter.cs:445)
  • ObiActor.DeactivateParticle(int) virtual (Common/Actors/ObiActor.cs:677)
  • ObiSoftbody, ObiSoftbodySurfaceBlueprint, ObiParticleAttachment (used by JellyfishSoftCore.cs) — unchanged.
  • Compile: live Unity Editor (which auto-refreshed and recompiled after the asset import + the YAML/script changes) reports 0 errors, 7 warnings — all 7 warnings are in Assets/Obi/Samples/Common/SampleResources/Scripts/CharacterController/ObiCharacter.cs for Obi's own sample using the deprecated Rigidbody.velocity API. None originate in our code, none are caused by the upgrade, all predate it.
  • Batch-mode compile attempt failed early (return code 1 in log) because the editor was holding the project lock — but the live editor's clean compile is a stronger signal.
  • What was NOT verified in this session: a Play-mode session in MatchScene to confirm fluid renders, particles emit/pool/are consumed by altars, and native memory stays flat over 5–10 minutes (the original leak signature). The asset author identifies the same root-cause we patched, so the fix is expected to hold, but the user should run a play session to confirm.

Files touched

  • Assets/ProtoV2/Scripts/BloodSystem/ObiLeakWorkaround.cs — DELETED
  • Assets/ProtoV2/Scripts/BloodSystem/ObiLeakWorkaround.cs.meta — DELETED
  • Assets/ProtoV2/Scenes/MatchScene.unity — removed component reference (line ~8285) and MonoBehaviour block (was lines 8458–8470)
  • C:/Users/ReconUnPro/.claude/projects/E--Unity-Projects-PrototypeV2/memory/MEMORY.md — added 2026-04-29 entry; appended supersession note to 2026-03-29 entry
  • docs/OBI_UPGRADE_7_1_1.md — pre-existing analysis still accurate (no edits this session)

Follow-ups

  • Play MatchScene for ≥5 min and watch native memory / D3D12 pool usage. If it climbs, restore ObiLeakWorkaround from git history and post on the Obi forum with our solver config.
  • Optional improvements unlocked since 7.1 (already available, not pursued in this upgrade):
  • Solver boundaryLimits could replace fluid-side ObiCollider walls in BoundaryWalls.cs (PhysX walls still needed for the player Rigidbody).
  • diffusionMask parameter on ObiSolver could let BloodAttributionTracker move team-attribution into a per-particle user-data channel instead of a managed dictionary.
  • Confirm static ObiColliders on FloorObiSurface / BoundaryWalls are flagged as such so they benefit from "static colliders not processed during ObiSolver.Update()" perf change.

Summary of Changes and Fixes

Obi Fluid Finalizer-Thread GraphicsBuffer Crash (Patched)

Symptom: Standalone builds were crashing at random after extended play due to a graphics buffer being disposed on a non-main thread, specifically the GC finalizer thread. Root Cause: ObiNativeList.cs had an incorrect implementation of the IDisposable pattern. The destructor (~ObiNativeList()) called Dispose(false), leading to GPU resource disposal on a non-main thread, causing Unity to crash. Explicit disposals did not suppress finalization, allowing the finalizer to still execute and cause crashes.

Fix: Modified the Dispose(bool disposing) method to only dispose of GPU resources when called from the main thread (i.e., when disposing is true). Updated the public void Dispose() method to call GC.SuppressFinalize(this), preventing the finalizer from executing if the object was explicitly disposed.

Verification: Patched file backed up for future reference. Changes aligned with Microsoft’s recommended IDisposable pattern, ensuring unmanaged resources are correctly managed. A long-session playtest is needed to confirm the crash has been resolved.

Follow-ups: Conduct a 30-minute uninterrupted session to verify no further crashes occur. Monitor for updates from Obi via Package Manager and adjust patches accordingly if upstream fixes are made.

Typing Sound System Integration

Font Change: Switched font in terminal screens from VT323 to ShareTechMono for better runtime rendering, as VT323 was too thick due to its bitmap nature.

Box-Drawing Character Fix: Replaced unsupported box-drawing characters with equals signs (=) in scripts, as ShareTechMono does not include these glyphs.

Countdown Sequence Redesign: Adjusted opacity and text alignment for better readability. Rewrote the countdown sequence to accumulate lines like a terminal log.

Typing Sound System: Integrated typing sound effects using GameAudioData for easier management and consistency across audio clips. Implemented sound throttling and pitch randomization for realism.

Verification: Ensured zero compile errors and confirmed scene saves. Checked that GameAudioData.terminalTypingSound is visible in the Inspector for assignment.

Follow-ups: Assign a suitable clip to terminalTypingSound. Test ShareTechMono at runtime and adjust font size if necessary. Consider adding terminalTypingSound to an auto-link map for easier management.

Additional Notes

The fixes and enhancements aim to improve stability, usability, and aesthetic consistency without impacting gameplay or visuals. Future updates from Obi should be monitored to determine if patches remain necessary. Assignments in the Inspector and runtime testing are crucial for ensuring all changes perform as expected.

Raw session notes

2026-04-20 — ExperimentBriefScene (Premise & Flavor Text)

What Changed

New scene: Assets/ProtoV2/Scenes/ExperimentBriefScene.unity — inserted into Build Settings at index 1 (between MainMenu and LobbyScene).

New script: Assets/ProtoV2/Scripts/ExperimentBriefController.cs

  • Builds all UI from code: full-screen black canvas, VT323 green typewriter text, blinking cursor
  • Randomised per-session: 5-digit experiment number (10000–99999), saved to PlayerPrefs["RitualRuin_ExperimentNum"] for ObjectiveBriefScene to read
  • Text sequence (clinical experiment-observer voice):
  • RITUAL & RUIN — EXPERIMENT SYSTEM v4.2
  • INITIALIZING SESSION...
  • EXPERIMENT #[NNNNN] — INITIATED
  • Variant selection: JELLYFISH — ALPHA SPECIMEN
  • Condition Alpha/Beta subject listing: A1/A2/B1/B2 with registry IDs G4-[NNNNN]-A1 etc.
  • Altar/zone status, COMMENCING SUBJECT CONFIGURATION...
  • Any key skips to completed text; any key on [ BEGIN CONFIGURATION ] transitions to LobbyScene
  • Input via UnityEngine.InputSystem — keyboard anyKey + gamepad aButton/bButton/startButton/selectButton
  • Font size: 32

Post-creation tweaks:

  • FONT_SIZE 22 → 32 (text was too small at runtime)
  • Subject labels changed to A1/A2/B1/B2; registry IDs use experiment number as the center digit group (G4-{expNum}-A1) instead of separate random pair IDs
  • Experiment number changed to 5-digit range (10000–99999)

Modified: Assets/ProtoV2/Scripts/MainMenuController.cs:174

  • OnStartClicked now loads "ExperimentBriefScene" instead of "LobbyScene"

New editor tool: Assets/ProtoV2/Scripts/Editor/ExperimentBriefSceneSetup.cs

  • Menu: Ritual & Ruin/Setup ExperimentBriefScene — idempotent, creates Camera + Light + Controller GO, saves scene, inserts into build settings

Why

User requested a flavor-text interstitial between MainMenu and LobbyScene. The design vault establishes a clinical experiment-observer tone with green terminal UI. The scene surfaces the experiment premise (creatures as engineered ritual vessels, not heroes) through cold procedural log output rather than exposition.

Verification

  • Unity compiled ExperimentBriefController.cs with zero errors
  • Setup script ran: Camera, Directional Light, ExperimentBriefController GO created and saved
  • Build settings confirmed: MainMenu(0) → ExperimentBriefScene(1) → LobbyScene(2) → ObjectiveBriefScene(3) → MatchScene(4)
  • MainMenuController.OnStartClicked confirmed routing to "ExperimentBriefScene"

Follow-ups

  • Play-test the typewriter pacing (cps values per line are tunable in ExperimentBriefController.Lines)
  • Could add CRT scanline overlay (CRTOverlayController) if the terminal aesthetic needs more texture
  • Future: swap [JELLYFISH] for actual selected variant once creature selection is implemented

2026-04-20 — ObjectiveBriefScene (Objective Briefing)

What Changed

New scene: Assets/ProtoV2/Scenes/ObjectiveBriefScene.unity — inserted at build index 3 (between LobbyScene and MatchScene).

New script: Assets/ProtoV2/Scripts/ObjectiveBriefController.cs

  • Same terminal aesthetic as ExperimentBriefController (green VT323, typewriter, blinking cursor)
  • Reads experiment number from PlayerPrefs["RitualRuin_ExperimentNum"] to match ExperimentBriefScene
  • Text covers: primary directive (collect/deliver blood), cooperative protocol, evolution sequence, terminal phase warning
  • Ends with [ EXPERIMENT COMMENCING ] → any key loads MatchScene

Modified: Assets/ProtoV2/Scripts/ExperimentBriefController.cs

  • Saves experiment number to PlayerPrefs["RitualRuin_ExperimentNum"] on Awake so ObjectiveBriefScene displays the same ID

Modified: Assets/ProtoV2/Scripts/LobbySceneLoader.cs:42

  • LoadMatchScene() now loads "ObjectiveBriefScene" instead of "MatchScene"

New editor tool: Assets/ProtoV2/Scripts/Editor/ObjectiveBriefSceneSetup.cs

  • Menu: Ritual & Ruin/Setup ObjectiveBriefScene — idempotent

Objective Text (exact lines)

Why

User requested a second interstitial explaining game objectives in the same clinical terminal style as ExperimentBriefScene. Text sourced from Onboarding System, Progression System, and Ritual System design docs. Corrected to not imply a single ritual guarantees evolution (bar must fill completely).

Verification

  • Zero compile errors after all changes
  • Setup script ran: Camera, Light, ObjectiveBriefController GO created and saved
  • Build settings confirmed: MainMenu(0) → ExperimentBriefScene(1) → LobbyScene(2) → ObjectiveBriefScene(3) → MatchScene(4)
  • LobbySceneLoader.LoadMatchScene() confirmed routing to "ObjectiveBriefScene"

Full Scene Flow (updated)

MainMenu → ExperimentBriefScene → LobbyScene → ObjectiveBriefScene → MatchScene

---

2026-04-20 — Pink Chunks After Scroll + Camera Scroll Distance

What changed

Pink chunks after scroll (material lifetime leak)

  • FloorOpacityController.cs — replaced blanket per-instance material destroy in OnScrollCompleted with a diff-based sync. Root cause: controller called Destroy(c.mat) on every tracked chunk on every scroll, but FloorManager.ScrollSequence reuses 3 of 4 floors (old Top → EmitterOnly, old Middle → Top, old Bottom → Middle, new → Bottom). Surviving MeshRenderer.material references pointed to destroyed Material objects → Unity error shader → solid magenta chunks on the 3 reused floors.
  • Added ChunkData.go field for GameObject-based lookup.
  • Added _registeredChunkGOs (HashSet) + _chunkByGO (Dictionary) for O(1) dedup.
  • Rewrote OnScrollCompleted: builds keep set from FloorManager.CurrentTop/Middle/Bottom, iterates _chunks in reverse, destroys material + removes tracking ONLY for chunks whose floor left the keep set, then calls TryRegister for the 3 current floors (dedup skips survivors, only new Bottom adds entries).
  • Added OnChunkReturnedToPool(GameObject) handler for lifecycle-correct disposal (fires when Floor.SetGap / Floor.DestroyFloor returns chunks to pool).
  • Split subscription flags (_subscribed for FloorManager, _fgSubscribed for FloorGenerator) for independent retry in Start.
  • OnFloorGenerated now early-returns when FloorManager.Instance.IsScrolling is true — prevents double-registration; OnScrollCompleted picks up the new Bottom floor.
  • OnDestroy clears _registeredChunkGOs + _chunkByGO.
  • FloorGenerator.csReturnChunkToPool now fires OnChunkReturnedToPool?.Invoke(chunk) before SetActive(false) and nulls the returned chunk's MeshRenderer.sharedMaterial as a latent-leak guard.
  • New public event: public event System.Action OnChunkReturnedToPool;

Camera scrolled 2 floors per scroll

  • ScrollingFloorCamera.cs:198-203ScrollCoroutine was lerping to an absolute CalculateFloorYPosition(nextFloorIndex), but Inspector-placed camera Y did not match CalculateFloorYPosition(0) because FloorManager deliberately disables SnapToFloor(0) at init. Result: first scroll teleport-corrected the offset + descended one floor = looked like 2 floors.
  • Replaced with relative delta:
  • Orthographic isometric projection maps camera Y 1:1 to world Y, so spacing delta is exact. No alignment drift possible.

Why

  • Pink chunks were a visible regression on every scroll — rendered scroll unusable in playtest.
  • 2-floor scroll broke the expected 1-floor cadence the scrolling floor loop is built around (3 visible floors + 1 emitter = 4-floor wheel, scroll event advances by 1).

Verification

  • Unity MCP read_console clean after both fixes (only unrelated Player 2 input warnings).
  • Manual review of OnScrollCompleted diff logic against FloorManager.ScrollSequence role rotation — keep-set matches exactly the 3 surviving floors post-rotation.
  • Camera delta verified algebraically against FloorVerticalSpacing (default 5).

Follow-ups (for user playtest)

  • Trigger 5+ consecutive scrolls, confirm no pink chunks.
  • Memory Profiler: Material instance count should stay bounded (~3 floors × gridW × gridH + delta), not grow unboundedly.
  • Confirm column occlusion fade (occludedChunkOpacity=0.15) still works on surviving Top floor after scroll.
  • Confirm camera descends exactly FloorVerticalSpacing units per scroll.

Files touched

  • Assets/ProtoV2/Scripts/FloorSystem/FloorOpacityController.cs
  • Assets/ProtoV2/Scripts/FloorSystem/FloorGenerator.cs (ReturnChunkToPool + new event)
  • Assets/ProtoV2/Scripts/CameraSystem/ScrollingFloorCamera.cs:198-203

Plan artifact

  • .omc/plans/fix-pink-chunks-after-scroll.md (5-step plan, used by executor)

---

Session 2 — Follow-up fixes from playtest

Black chunks regression (from Session 1 fix)

The sharedMaterial = null guard I added in FloorGenerator.ReturnChunkToPool and the per-instance material destruction in FloorOpacityController.OnScrollCompleted + OnChunkReturnedToPool broke the pool recycle path. When a chunk came back out of the pool for a new floor, mr.material had nothing to clone (sharedMaterial null) or was pointing at a destroyed Material → black/pink chunks on recycled tiles.

Insight: the floor chunk pool is bounded (~4 floors × grid cells). Per-instance materials live permanently with their pool chunks — never destroy them, just update tracking.

  • FloorGenerator.cs:956-961 — removed the var mr = chunk.GetComponent(); if (mr != null) mr.sharedMaterial = null; guard. Pool chunks retain their per-instance material.
  • FloorOpacityController.cs:334 — removed if (c.mat != null) Destroy(c.mat); from OnScrollCompleted. Tracking removal preserved.
  • FloorOpacityController.cs:369 — removed if (data.mat != null) Destroy(data.mat); from OnChunkReturnedToPool. Tracking removal + floor-prune preserved.

Gap-punched chunks still had colliders (players couldn't fall through new holes)

FloorGenerator.AddFloorObiSurface bakes a floor-wide MeshCollider once at generation, using the floor's gap grid at that moment. When Floor.OpenGapsForMiddleRole later punches new gaps via SetGap (on scroll, bottom→middle transition), the visual chunks return to pool but the baked collider mesh still has quads at those cells. Players collided with invisible geometry instead of falling through.

  • FloorGenerator.cs — added public void RebuildObiSurfaceMesh(Floor floor) after AddFloorObiSurface. Locates FloorObiSurface child, destroys the old mesh, rebuilds via existing BuildSolidChunkMesh(floor), assigns to MeshCollider.sharedMesh.
  • Floor.csOpenGapsForMiddleRole calls FloorGenerator.Instance.RebuildObiSurfaceMesh(this); after the SetGap loop.

New-Top floor had no emitters after scroll

Emitters were only instantiated when GenerateFloor(..., emitterOnly: true) fired, which only runs for the initial above-view emitter floor. On scroll, old-Top got promoted to EmitterOnly role, but its Floor.emitters list was empty (it was generated as a playable floor, not emitter-only) → ActivateEmitters() iterated nothing → no blood.

Refactored to a fixed global pool, decoupling emitters from Floor lifetime.

  • NEW Assets/ProtoV2/Scripts/BloodSystem/BloodEmitterPool.cs — singleton MonoBehaviour, owns 3 persistent BloodEmitter instances under obiSolverParent (matches emittersPerFloorMax=3). ActivateOn(Floor) repositions N emitters to floor.EmitterPositions grid cells and activates them. DeactivateIfActive(Floor) / DeactivateAll() turn them off.
  • Floor.cs — removed emitters list field, RegisterEmitter, ActivateEmitters, DeactivateEmitters, Emitters property. SetRole switch now routes through BloodEmitterPool.Instance?.ActivateOn(this) / DeactivateIfActive(this). ActivateEmittersAfterDelay coroutine still preserves the pre-match isActivated && MatchRunning gate. DestroyFloor no longer destroys emitter GOs (pool emitters persist).
  • FloorGenerator.cs — stopped instantiating emitter prefabs in GenerateFloor. Added private helper PickCellPositions(Floor, int, int, HashSet) (pure cell picker, no instantiation). Every floor now gets EmitterPositions populated at generate-time (not just emitterOnly floors). Altar exclusion logic unchanged. Dead emitterPrefab + obiSolverParent fields left on FloorGenerator to avoid Inspector reference breakage — user will clean up later.
  • EmitterIndicatorController.cs — rewritten. Was drawing rings based on each floor's own (empty) EmitterPositions on Top/Middle/Bottom. Now draws rings on CurrentTopFloor at CurrentEmitterFloor.EmitterPositions (where blood actually lands). Subscribes only to OnScrollSequenceCompleted for rebuild.

Why Option C (pool) over Option A (lazy spawn) / Option B (always instantiate)

  • A would Destroy + Instantiate N emitters every scroll → GC pressure, expensive.
  • B would keep 4 floors × N emitters alive → memory waste on inert GOs.
  • C (chosen) keeps exactly 3 emitters alive for the whole match, repositioned on scroll. Zero per-scroll allocations. Emitters already lived under obiSolverParent (not under floors), so "reparent on migrate" wasn't even needed — just transform.position writes.

Manual Inspector step (not automated)

User must add a BloodEmitterPool GameObject to MatchScene and wire:

  • emitterPrefab → same BloodEmitter prefab currently on FloorGenerator
  • obiSolverParent → same Transform currently on FloorGenerator
  • poolSize=3, emitterYOffset=0 (defaults)

Verification

  • Unity read_console after each fix: 0 errors. Pre-existing unrelated warnings only (Player 2 input, obsolete FindObjectOfType in other files).
  • BloodEmitter.SetEmitterActive(bool) confirmed safe to call while repositioning via transform.position writes — emitter reads its own position via ObiEmitter each frame.
  • BloodEmitterIndicator confirmed reposition-safe (reads transform.position per frame, useWorldSpace=true).

Follow-ups for user playtest

  • Initial floor should have blood within ~0.5s of match start (existing activation delay).
  • After one scroll: new EmitterOnly (old-Top) should produce blood on new Top (old-Middle). Indicator rings should move to new Top at the new emitter floor's grid cells.
  • After 5+ scrolls: no chunk leaks, no pink/black chunks, blood still emits from correct floor.
  • Confirm gap-punched chunks on the new Middle floor (old-Bottom post-scroll) are walkable/fallable through — no invisible colliders.

Files touched (this session)

  • Assets/ProtoV2/Scripts/BloodSystem/BloodEmitterPool.cs (new)
  • Assets/ProtoV2/Scripts/FloorSystem/Floor.cs (emitter decoupling + RebuildObiSurfaceMesh call)
  • Assets/ProtoV2/Scripts/FloorSystem/FloorGenerator.cs (pool migration, PickCellPositions, RebuildObiSurfaceMesh, ReturnChunkToPool guard removal)
  • Assets/ProtoV2/Scripts/FloorSystem/FloorOpacityController.cs (material-destroy removal in OnScrollCompleted + OnChunkReturnedToPool)
  • Assets/ProtoV2/Scripts/FloorSystem/EmitterIndicatorController.cs (rebuilt to draw on Top using EmitterOnly's positions)

---

2026-04-20 — Transition Cleanup + Rebinder Dedup + AutoTilt

What changed

Pause/transition hardening

  • PauseController.csBuildUI() now scans Resources.FindObjectsOfTypeAll() for the one owning PauseMenuPanel (was FindObjectOfType() — non-deterministic in builds because DontDestroyOnLoad canvases coexist: SceneTransition, CRTOverlay, per-player BarCanvas). Added _preBypassTimeScale (captures pre-pause timescale, restored on Resume — fixes outlast scale clobber). Added null-guard in Pause() — refuses to freeze if _pauseMenuUI is null. Added PauseMenuPanelName const.
  • PauseInputHandler.cs — Escape / gamepad Start now toggles pause/resume (was pause-only, unresumable).
  • MainMenuController.csAwake() scopes canvas scan to active scene's ScreenSpace canvas (fixes Abort→MainMenu breakage when SceneTransition canvas was picked up). Whitelisted child wipe to MainPanel/SettingsPanel consts only — designer-authored children now survive scene load.
  • CanvasGroupFade.csOnDisable resets interactable/blocksRaycasts based on target alpha. Prevents stranded non-interactable panels after rapid fade toggles.
  • DevPanel.cs — Awake gate hides GO via #if !UNITY_EDITOR && !DEVELOPMENT_BUILD. FlushObiButton removed (GO + wiring); FlushObiRoutine kept (Restart still calls it).
  • MatchManager.cs — inline comment documents EndMatch/pause interaction (gameplay can't reach EndMatch while paused; inputs disabled + coroutines halted).
  • MultiplayerManager.csgameStarted idempotency guard on StartGame(). 4 simultaneous Start presses now fire once.

Dead UGUI rebinder deleted

Project had two parallel rebinder stacks — the UI Toolkit one (InputRebindUIController + InputRebindPanel.uxml/.uss, the real phosphor-green-on-black one in LobbyScene) and a legacy UGUI one that was never wired into any scene. Deleted the entire legacy stack:

  • Assets/ProtoV2/Scripts/UI/InputRebindMenuUI.cs + .meta
  • Assets/ProtoV2/Scripts/UI/PlayerRebindPanel.cs + .meta
  • Assets/ProtoV2/Scripts/UI/BindingRowUI.cs + .meta
  • Assets/ProtoV2/Scripts/UI/RebindUI.cs + .meta
  • Assets/ProtoV2/Scripts/UI/PlayerSettingsPanel.cs + .meta (per-player modal)
  • Assets/ProtoV2/Scripts/UI/PlayerPanelSettingsButton.cs + .meta (mouse click path)
  • Assets/ProtoV2/Scripts/PlayerMenuController.cs + .meta (toggle-into-menu-mode)
  • Assets/ProtoV2/Prefabs/UI/BindingRow.prefab + .meta
  • 4 editor wizards: PlayerSettingsPanelSetup, PlayerMenuSetupWizard, MenuHintWiringWizard, InputRebindMenuSetup, RebindUISetup, FixWaitingTextReferences
  • Scene cleanup: PlayerSettingsPanelCanvas GO removed from LobbyScene; MenuHintText removed from PlayerPrefab/BarCanvas; PlayerMenuController component removed from PlayerPrefab root; legacy EventSystem removed from LobbyScene; SuspendMovement method removed from MultiplayerCharacterInput.cs; serialized playerSettingsPanel field removed from JoinScreenUI.cs.

Kept alive: InputRebindUIController, WorldSpaceRebindUI (UIDocument wrapper), InputRebindManager (backend), InputRebindPanel.uxml/.uss.

AutoTilt toggle on UI Toolkit rebinder

  • Assets/ProtoV2/UI/InputRebindPanel.uxml — 4 nodes, one per player section, after preset-dropdown-{i+1}.
  • Assets/ProtoV2/UI/InputRebindPanel.uss — new .autotilt-toggle styles in phosphor-green palette. :disabled state uses #008F11 dim green for clear visual distinction from unchecked.
  • InputRebindUIController.cs — cached toggles + handlers. RefreshAutoTiltToggle(i) called from the existing RefreshPlayerBindingDisplay chain (auto-syncs on input-mode change / device change / rebind / reset). Keyboard players: value=true, SetEnabled(false) (cosmetic only — PourController already forces auto-tilt for keyboard). Controllers: interactive, reads/writes PlayerPrefs.AutoTilt_Player_{i} + calls PlayerSetup.SetAutoTiltEnabled(). Uses SetValueWithoutNotify to avoid spurious writes on initial load. Handlers unregistered on refresh + OnDestroy.

Why

  • Pause menu was completely broken in builds (froze game, no menu) due to canvas-lookup non-determinism. MainMenu broke the same way on Abort.
  • Outlast phase timescale was lost on pause/resume.
  • DevPanel's Restart/Scroll/FlushObi were shipping in release builds.
  • Two parallel rebinder stacks was pure tech debt — the UGUI one was never reachable and its PlayerSettingsPanel popup was introducing a shared-panel fan-out bug where P2 opening a panel that P1 had open caused both CloseMenu handlers to fire.
  • AutoTilt setting was accessible only through the erroneous popup; removing the popup stranded the setting. Now lives correctly inside the per-player rebind panel.

Verification

  • Architect review APPROVED after each phase.
  • Unity compile clean (0 errors after each task).
  • MCP play-mode sanity: MainMenu, LobbyScene, MatchScene all enter play cleanly. Only pre-existing "Cannot find matching control scheme" warnings remain (4 PlayerPrefabs auto-spawn with limited devices — unrelated).
  • MainPanel + SettingsPanel procedural build confirmed in MainMenu play mode.
  • PauseMenuPanel + pause Card + settings sub-panel Card confirmed in MatchScene play mode.
  • PlayerSettingsPanel, MenuHintText, FlushObiButton confirmed 0 hits (fully deleted).
  • InputRebindUIController confirmed 1 hit in LobbyScene (live, intact).

Follow-ups

  • Manual playtest required: rebind a key via the UI Toolkit panel; toggle AutoTilt on a controller player, exit lobby + re-enter, confirm persistence; run full pause-during-outlast → 30s wait → resume → confirm EndMatch fires with Time.timeScale=1.
  • Deferred work: multi-player gamepad navigation of the UI Toolkit rebinder. Current rebinder is mouse-only by design (DisableKeyboardAndGamepadNavigation at InputRebindUIController.cs:773-816). Proper multi-player nav needs Option B (per-player focus routing layer listening to per-player PlayerInput.user.actions) plus 9 design decisions — separate session.
  • PauseController side-note: quit-from-pause momentarily applies pre-pause timescale for 1 frame before scene load. Masked by SceneTransition fade. Not fixed.
  • Minor: DevPanel Awake gate missing explicit return; after SetActive(false) — harmless now, defensive to add later.
  • Minor: redundant PlayerPrefs write in AutoTilt controller branch (both InputRebindUIController and PlayerSetup.SetAutoTiltEnabled write the same key). Identical value, no race. Can consolidate later.

---

2026-04-21 — Countdown UI Phosphor Styling + Shadow Tuning

What Changed

Modified: Assets/ProtoV2/Scripts/MatchCountdown.cs

  • StartCountdown() now applies terminal aesthetic at runtime (no Inspector wiring needed):
  • countdownPanel RectTransform stretched to full screen (anchorMin=0,0 / anchorMax=1,1, offsets zeroed)
  • Panel Image set to Color(0,0,0,0.8) — 80% opaque black overlay
  • countdownText color set to TypographyLibrary.PhosphorGreen; TypographyLibrary.ApplyVT323() applies VT323 font + glow material
  • Auto-sizing enabled: fontSizeMin=48, fontSizeMax=160, word wrap off, overflow mode Overflow, centered
  • Countdown text: "EXPERIMENT INITIATING IN {i}" (unchanged); GO text: "EXPERIMENT INITIATED"

Modified: Assets/ProtoV2/Scenes/MatchScene.unity

  • Directional Light m_Strength: 10.35 — shadow is noticeably lighter/less oppressive

Modified: Assets/Settings/PC_RPAsset.asset

  • m_MainLightShadowmapResolution: 2048512 — lower resolution produces softer/blurrier shadow edges (complements existing soft shadow quality=3)

Why

  • Countdown panel needed to match the green phosphor terminal aesthetic established by ExperimentBriefScene and ObjectiveBriefScene — previously used default white UI.
  • Ground shadow at full strength (1.0) and 2048px resolution was too sharp and visually heavy for the top-down arena; 0.35 strength + 512px resolution gives a softer, more ambient-feeling shadow.

Verification

  • MatchCountdown.cs compiled with zero errors
  • Scene YAML edits confirmed: strength 0.35 present in MatchScene.unity Directional Light node, resolution 512 present in PC_RPAsset.asset

Follow-ups

  • Play-test shadow strength — 0.35 is a starting point; tune up/down in Inspector if needed
  • Shadow resolution can be raised back toward 1024 if softness looks too blurry at runtime
  • Countdown panel font size range (48–160) may need tuning based on text length at different resolutions

---

2026-04-22 — Bug Fix Sweep (Full Codebase)

What Changed

Comprehensive bug fix sweep across 21 files based on deep architectural analysis. Organized into 3 parallel waves.

Wave 1 — Critical

MatchManager.cs:132-134

Added Time.timeScale = 1f to OnDestroy(). Prevents outlast-phase elevated timescale from bleeding into a rematch.

DeathHandler.cs:49

Replaced transformController.Toggle() with transformController.DisableCarrierForm() on player death. Toggle was firing animations, OnToggle events, and CancelPour — all unwanted when dying. DisableCarrierForm is the correct targeted method.

Fixed duplicate // 5. comment numbering → // 6.

UnifiedBar.cs

  • CreateWhiteSprite() now uses a static Sprite _whiteSprite cache. Was allocating a new Texture2D + Sprite per player per scene load (memory leak).
  • Camera.main null-check added with FindAnyObjectByType() fallback.
  • isDecayActive changed from public to [SerializeField] private _isDecayActive — SetDecayActive setter is the public API.
  • Color thresholds 0.3f / 0.65f promoted to [SerializeField] private float _criticalThreshold / _warningThreshold.

JellyfishVisuals.cs

  • Landing detection: added _wasDescending = false disarm in the fly-pulse path so spurious landing pulses can't fire after a mid-fall impulse.
  • Removed dead waveFrequency, waveAmplitude, wavePhaseStep fields + pragma disable.
  • Gated landing Debug.Log behind #if UNITY_EDITOR.
  • Replaced Shader.Find primary path with [SerializeField] private Shader _unlitShader + fallback. Inspector action required: assign Universal Render Pipeline/Unlit to the new field on PlayerPrefab's JellyfishVisuals component.

PauseController.cs

  • Added if (IsPaused) return; guard at top of Pause(). Double-call (controller disconnect + Esc) was capturing timeScale=0 as pre-pause value, making Resume freeze the game.
  • Replaced Resources.FindObjectsOfTypeAll() with Object.FindObjectsByType(FindObjectsInactive.Include, FindObjectsSortMode.None).

AudioManager.cs

  • Moved singleton subscriptions from Start() into a LateStart() coroutine that yields one frame first. Prevents race condition where AudioManager subscribed before other managers' Awake ran, silently missing events.

Wave 2 — High

MatchCountdown.cs

  • Added BloodAttributionTracker.ClearAll() at start of StartCountdown() — clears stale particle owner data from previous matches.
  • Changed Mathf.RoundToInt(countdownDuration)Mathf.FloorToInt so 3.6s shows 3-2-1 not 4-3-2-1.
  • AudioSource mixer fix already present (uses _audioData.uiGroup). No change needed.

ExperimentBriefController.cs / ObjectiveBriefController.cs

  • Mixer fix already present in both. No changes needed in Wave 2 (refactored in Wave 3).

PourController.cs

  • visualRoot renamed to [SerializeField] private _visualRoot (all 15 internal usages updated).
  • Altar list cached in Start() via RefreshAltarCache(), refreshed on FloorManager.OnFloorGenerated. Eliminates per-frame FindObjectsByType() × 4 players.
  • GetNearestHole() now early-returns if !_transformController.IsCarrierMode — skips grid scan when irrelevant.
  • Removed dead IsTransitioning branch (always-false).

PourTargetIndicator.cs

  • Glow GO (Light + sphere) now created once in Awake(), shown/hidden via SetActive. Was creating/destroying on every target change.
  • [SerializeField] private Shader _unlitShader replaces Shader.Find as primary path. Inspector action required.

PourSetupWizard.cs (Editor)

  • Updated reflection lookup from "visualRoot" + Public to "_visualRoot" + NonPublic to match renamed field.

FloorManager.cs

  • Added public event Action OnFloorDestroyed — fired before floor is destroyed in ScrollSequence().

ProgressionManager.cs

  • Subscribes to FloorManager.OnFloorDestroyed. Handler calls UnsubscribeFromAltar() for each altar on the destroyed floor, fixing altar event handler closure leaks.

Wave 3 — Medium + Cleanup

AltarParticleConsumer.cs

  • Added public float RitualCountdownTime => ritualCountdownTime; getter.

AltarCountdownVisual.cs

  • _totalTime now reads from _consumer.RitualCountdownTime instead of hard-coded 3f. Pulse ramp stays in sync with Inspector value.

CharacterController1.cs

  • PhysicsMaterial changed to private static _sharedPhysicsMaterial. Created once across all players, not per-player per scene load.

VerticalHazard.cs

  • Removed two SendMessage("SetInvulnerable", ...) calls — no component implements the receiver. Was a silent no-op every frame.

HoleAlignmentHelper.cs

  • Removed dead minUpwardSpeed field + surrounding pragma disable/restore.

MultiplayerManager.cs

  • All 13 plain Debug.Log calls wrapped in #if UNITY_EDITOR || DEVELOPMENT_BUILD.

MainMenuController.cs

  • Resources.FindObjectsOfTypeAll()Object.FindObjectsByType(...).
  • Process.GetCurrentProcess().Kill()Application.Quit() (editor path kept as EditorApplication.isPlaying = false).

BriefingScreenBase.cs (NEW)

Assets/ProtoV2/Scripts/UI/BriefingScreenBase.cs — abstract MonoBehaviour base for briefing screens. Contains all shared logic: BuildUI, TypeRoutine, CompleteTyping, Redraw, RevealPrompt, UnlockAfter, PlayTypingSound, MakeStretch, MakePivot. Abstract: Lines, NextSceneName, PromptText. Virtual: PromptWidth, OnAwakeInit().

ExperimentBriefController.cs — reduced from 310 → 54 lines, now inherits BriefingScreenBase.

ObjectiveBriefController.cs — reduced from 297 → 50 lines, now inherits BriefingScreenBase.

Why

Deep architectural analysis identified 30+ issues across all severity levels. All gameplay behaviour preserved — repair only, no new features.

Verification Performed

  • All agents read files back after editing to confirm diffs
  • Wave 2-F and Wave 2-E confirmed several audio fixes were already in place (no duplicate work)
  • PourSetupWizard reflection updated to match renamed private field (grep confirmed no other external references)

Follow-ups / Inspector Actions Required

1. JellyfishVisuals on PlayerPrefab: Assign Universal Render Pipeline/Unlit shader to the new _unlitShader field in the Inspector

2. PourTargetIndicator on PlayerPrefab: Assign Universal Render Pipeline/Unlit shader to the new _unlitShader field

3. UnifiedBar thresholds: _criticalThreshold (0.3) and _warningThreshold (0.65) now appear in Inspector — verify defaults are correct

4. Full play-through test: Rematch flow (timeScale), Carrier-mode death (no Toggle side effects), pause double-call, pour target performance

---

2026-04-22 — Floor System: Emitter Pool Wiring + Warning Cleanup

What changed

BloodEmitterPool wired into MatchScene

Previous session (2026-04-20) created BloodEmitterPool.cs but the GO wasn't in the scene yet → BloodEmitterPool.Instance was null → no blood emission after scroll.

  • Via Unity MCP: created BloodEmitterPool GameObject in MatchScene, attached component, wired:
  • emitterPrefab → BloodEmitter prefab (guid 7fd060167c5b98a4188e3843d8383646, same asset previously on FloorGenerator)
  • obiSolverParent → ObiSolver transform (instance 60412)
  • poolSize = 3 (default), emitterYOffset = 0 (default)
  • Scene saved.

EmitterIndicatorController disabled

Red ring floor-tile debug overlay (EmitterIndicatorController GO) disabled in MatchScene — debug-only, not needed at runtime. BloodEmitterIndicator burst-countdown circles on pool emitters remain active.

Compiler warnings cleared

All 18 warnings confirmed resolved (stale console from prior session). After forced recompile: 0 warnings, 0 errors. Fixes had been applied in the 2026-04-20 session:

  • FindObjectOfTypeFindFirstObjectByType / FindObjectsByType(..., FindObjectsSortMode.None) across JellyfishSoftCore, InputBindingPersistenceManager, DevPanel, AudioMixerSetupWizard.
  • enableWordWrappingtextWrappingMode in MatchCountdown + AlphaSetupWizard.
  • PreventDefault()StopPropagation() in InputRebindUIController (×4).
  • Unused [SerializeField] fields in HoleAlignmentHelper + JellyfishVisuals suppressed with #pragma warning disable CS0414.

Why

  • Pool GO missing = no blood, which broke the entire emitter pool refactor from the previous session.
  • Debug ring overlay cluttered the game view with no gameplay value.
  • Zero-warning build ensures no silent regressions from deprecated API drift.

Verification

  • Unity console: 0 errors, 0 warnings after forced recompile.
  • BloodEmitterPool.Instance non-null confirmed via MCP scene inspection.

Follow-ups for user playtest

  • Enter Play → confirm blood emits on initial floor within ~0.5s of match start.
  • Trigger scroll → confirm new EmitterOnly floor (old Top) emits blood onto new Top (old Middle). No double-spawn, no stale emitters.
  • Confirm gap-punched holes on Middle floor are physically passable (floor collider rebuild fix from 2026-04-20 Session 2).

Files / scene touched

  • Assets/ProtoV2/Scenes/MatchScene.unity — BloodEmitterPool GO added + wired; EmitterIndicatorController GO set inactive
  • No script files modified this session (all script changes were 2026-04-20)

---

2026-04-22 — Terminal UI Polish + Typing Sound + GameAudioData Integration

What Changed

Font

Modified: ExperimentBriefController.cs, ObjectiveBriefController.cs, MatchCountdown.cs

  • All three terminal screens switched from VT323 → ShareTechMono (ApplyVT323ApplyShareTechMono)
  • VT323 rendered too thick at runtime due to its bitmap nature; ShareTechMono is a proper monospace sans-serif with thinner strokes

Box-Drawing Character Fix

Modified: ExperimentBriefController.cs, ObjectiveBriefController.cs

  • ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ (U+2501) → ========================================
  • ShareTechMono does not include box-drawing glyphs; Unity was substituting at runtime

Countdown Sequence Redesign

Modified: MatchCountdown.cs

  • Panel opacity: 0.8 → 0.9
  • Text alignment: Center → TopLeft
  • Complete sequence rewrite — accumulating log style (all lines stay on screen):
  • Each line types in via AppendTyped() coroutine (80 CPS); numbers hold ~1s cadence; final line shows world timestamp (CYCLE IV = matches experiment system v4.2, T{dayOfYear} = clinical day reference)
  • OnCountdownBeat still fires per number for external feedback hooks

Typing Sound System

Modified: GameAudioData.cs — added public AudioClip terminalTypingSound under UI header

Modified: ExperimentBriefController.cs, ObjectiveBriefController.cs, MatchCountdown.cs

  • All three: replaced bare [SerializeField] AudioClip typingSound with [SerializeField] GameAudioData _audioData
  • AudioSource created at runtime; clip = _audioData.terminalTypingSound, mixer group = _audioData.uiGroup
  • Throttled to max 20 clicks/sec (Time.time gate of 0.05s) with ±6% pitch randomization per character
  • Recommended clip: iface_tick_001.ogg or iface_tick_002.ogg (Kenney Interface Sounds, CC0, already in project at SFX/Options/AltarTick/)

Wired via MCP:

  • ExperimentBriefSceneExperimentBriefController GO: _audioData = GameAudioData.asset
  • ObjectiveBriefSceneObjectiveBriefController GO: _audioData = GameAudioData.asset
  • MatchSceneMatchCountdown GO: _audioData = GameAudioData.asset

Pending: assign terminalTypingSound clip in GameAudioData.asset Inspector

CLAUDE.md Rule Update

Modified: CLAUDE.md rule #6

  • Now mandates GameAudioData for ALL audio — no bare AudioClip fields, no Resources.Load
  • Explicit pattern: scripts take [SerializeField] GameAudioData _audioData, read _audioData.clipName + _audioData.sfxGroup / _audioData.uiGroup

Why

  • VT323 was too visually heavy; ShareTechMono matches the clinical precision of the experiment-observer aesthetic better
  • Countdown needed to read as a live terminal log rather than a flashing number — accumulating lines match the ExperimentBriefScene style
  • Typing sound adds tactile presence to the typewriter effect; routed through GameAudioData so clip is swappable from one place
  • Rule #6 tightened so future audio won't bypass the volume sliders

Verification

  • Zero compile errors (confirmed via MCP read_console)
  • All three scene saves confirmed via MCP
  • GameAudioData.terminalTypingSound field visible in Inspector (assign clip to activate sound)

Follow-ups

  • Assign iface_tick_001.ogg to terminalTypingSound on GameAudioData.asset
  • Audition ShareTechMono at runtime — tune font size if needed (FONT_SIZE = 32f in brief controllers, fontSizeMin=48 / fontSizeMax=160 in countdown)
  • Consider adding terminalTypingSound to GameAudioDataWizard auto-link map (field name → filename hint)

---

2026-04-23 — Obi Fluid Finalizer-Thread GraphicsBuffer Crash (Patched)

Symptom

Standalone builds crashed at random after extended play. Last two crashes: 2026-04-19 ~22:56 and ~23:39. Crash handler reports at %LOCALAPPDATA%\Temp\DefaultCompany\PrototypeV2\Crashes\Crash_*/; stack trace tail:

Root cause

Assets/Obi/Scripts/Common/DataStructures/NativeList/ObiNativeList.cs ships a broken IDisposable pattern:

1. ~ObiNativeList() calls Dispose(false), which unconditionally invokes DisposeOfComputeBuffer()m_ComputeBuffer.Dispose(). Unity requires GPU resources to be disposed on the main thread; the GC finalizer thread is not the main thread, so Unity hard-crashes in GraphicsBuffer::DestroyBuffer_Injected.

2. public void Dispose() does not call GC.SuppressFinalize(this), so the finalizer still fires even for explicitly-disposed lists.

Ad-hoc new ObiNativeList(...) allocations exist in multiple places (e.g. DynamicRenderBatch.cs:320, ProceduralRenderBatch.cs:152, rendering systems for the Compute fluid backend). When any of these are GC'd without explicit disposal, the finalizer fires off-thread and crashes.

The March 2026 D3D11 mitigation (project_obi_memory_fix.md) addressed a separate GPU memory leak, not this crash. The memory note's claim that the finalizer was "mitigated by D3D11, not blocking" turned out to be wrong.

Previous NullReferenceException spam in FloorImpactAudio.HandleBloodSpilled and SceneTransition.Update (seen in Player-prev.log) wasn't the crash cause but made it more frequent — each caught exception added GC pressure, shortening the window before the orphaned ObiNativeList was collected. Both NullRefs were independently fixed in the earlier bug-fix sweeps (2026-04-22_BugFixSweep.md).

Fix

Patched Assets/Obi/Scripts/Common/DataStructures/NativeList/ObiNativeList.cs to the standard textbook IDisposable pattern:

  • Dispose(bool disposing) now gates DisposeOfComputeBuffer() behind the disposing flag, so only explicit main-thread callers ever touch the GraphicsBuffer. Unmanaged memory (UnsafeUtility.Free) is still freed on both paths.
  • public void Dispose() now calls GC.SuppressFinalize(this).

Net effect: the finalizer never touches GPU resources. Worst case is a small leaked GraphicsBuffer per orphaned list until the next scene load — vastly preferable to a hard crash.

Patch bookkeeping

  • Original file backed up at patches/ObiNativeList.cs.original
  • Revert instructions + re-apply notes documented at patches/README.md
  • Two lines changed in Dispose(bool), one line added in Dispose() (see patch comment in file at line 130)

Verification

  • Diff against backup confirms only the intended two changes.
  • Patch follows the Microsoft-documented IDisposable pattern (Dispose(bool) with GC.SuppressFinalize); same correction is applied across countless native-interop wrappers in the wild.
  • Build/play verification still pending — needs a long-session playtest to confirm the crash is gone. Earlier crash reproduced roughly every 10–20 minutes under active play; a 30-minute uninterrupted session with floor-scroll + combat should cover it.

Follow-ups

  • TODO: Run a ≥30-minute session build to confirm the crash is eliminated. Capture full Player.log to verify no finalizer-stack errors remain.
  • TODO: If Obi is ever updated via Package Manager, diff the new ObiNativeList.cs against patches/ObiNativeList.cs.original — if upstream fixed it, remove the patch; otherwise re-apply.
  • No impact on gameplay/visuals expected. This is a lifecycle correctness fix only.

Certainly! Let's break down the two sessions into structured summaries with key points and implications.

Session 1: Animation and UI Refinement

Goals: Ensure consistent stroke thickness across corner marks in all pivots. Provide visible hover feedback on buttons without relying on bloom effects. Remove redundant brackets from button labels, as corner marks provide visual cues. Eliminate the subtitle "SURVIVAL IS MEASURED, NOT CELEBRATED" from the main menu. Document a pixel-perfect rule to prevent future UI rendering issues.

Key Actions and Changes: Corner Marks Consistency: Adjusted CornerMark_* sprites to maintain uniform stroke thickness across all pivots by modifying their source dimensions and downscaling properties.

Hover Feedback: Implemented hover variants for corner marks with extended arms. Altered button background color (BgHover) for clearer visibility during hover states.

UI Builder Enhancements: Integrated EnsurePixelPerfectCanvas() to automatically enforce pixel-perfect rendering on any canvas using UIBuilder components, ensuring consistent UI appearance regardless of CanvasScaler settings.

Content Adjustments: Removed [ ] brackets from button labels. Deleted the main menu subtitle as per user decision.

Documentation: Added documentation for the pixel-perfect rule in CLAUDE.md, docs/UI_PIXEL_PERFECT.md, and UIBuilder's class-header comment to prevent future UI issues.

Validation: Confirmed through compile checks, visual inspections at 1920×1080 resolution, and grep searches that changes were correctly implemented. Follow-ups include testing hover interactions with a real mouse cursor and updating documentation examples.

Importance: Ensures consistent UI rendering across different display scales and prevents future subpixel-rounding issues by enforcing the pixel-perfect rule through UIBuilder.

Session 2: Menu Polish

Goals: Achieve uniform stroke thickness for corner marks. Ensure hover feedback is visible without bloom effects. Remove brackets from button labels, as they are redundant with corner marks. Eliminate a specific subtitle from the main menu. Document the pixel-perfect rule to prevent future UI rendering issues.

Key Actions and Changes: Sprite Adjustments: Increased STROKE dimension in CornerMarkSpriteBuilder.cs for consistent rendering across pivots. Added hover variants with extended arms to enhance visual feedback.

UI Builder Enhancements: Implemented EnsurePixelPerfectCanvas() in UIBuilder methods to enforce pixel-perfect rendering automatically.

Button Behavior Modifications: Updated button color and sprite swapping logic in ClinicalButton.cs to improve hover visibility. Removed scale tween on corner marks to avoid subpixel distortion.

Content Adjustments: Stripped brackets from button labels in various controllers and builders. Removed the main menu subtitle as per user decision.

Documentation: Documented the pixel-perfect rule in multiple locations for easy reference and enforcement.

Validation: Verified through compile checks, visual inspections, and grep searches to ensure changes were implemented correctly. Follow-ups include testing hover interactions with a real mouse cursor and updating documentation examples.

Importance: Ensures consistent UI rendering across different display scales and prevents future subpixel-rounding issues by enforcing the pixel-perfect rule through UIBuilder.

These sessions highlight the importance of meticulous attention to detail in UI design, ensuring consistency and preventing recurring issues. The implementation of automated checks and comprehensive documentation serves as a safeguard against potential regressions.

Raw session notes

2026-04-17 — Prefab Cleanup, Spawn Fix, Ground-Anchor Restructure

Three related problems tackled in one session: non-random spawn on first scene load, continuous character bouncing in LobbyScene, and a messy PlayerPrefab with dead tier placeholders and a mis-purposed ground collider.

1. Spawn randomization not applied on first MatchScene load

Symptom: Players spawned in deterministic circle positions on first load from LobbyScene. Randomization worked correctly only on scene reload (rematch).

Root cause: Two interacting timing issues.

1. MultiplayerManager.Awake() configured PlayerInputManager with JoinPlayersWhenButtonIsPressed and subscribed onPlayerJoined. Joining wasn't disabled until Start(). On first load from LobbyScene, the "Start Game" button press was still in Unity's input buffer and got processed as a rogue join between Awake() and Start(), creating a player at a deterministic GetSpawnPosition() — which then pre-empted SpawnAllPlayers() via the duplicate-index check.

2. FloorManager.LateUpdate() called SpawnManager.SpawnPlayers() on frame 1. InputBindingPersistenceManager.ApplyBindingsNextFrame() ran on frame 2 and could trigger re-pairing callbacks that left players mispositioned.

On rematch the button press was consumed in the previous scene, so no rogue joins.

Fix

  • MultiplayerManager.Awake(): Added playerInputManager.DisableJoining() immediately after SetupPlayerInputManager() when autoSpawnAllPlayers=true. Closes the Awake→Start race window.
  • FloorManager.cs: Replaced _pendingSpawn + LateUpdate with a DeferredSpawn() coroutine that waits two frames before calling SpawnManager.SpawnPlayers(). Ensures randomization runs after InputBindingPersistenceManager finishes.
  • SpawnManager.cs: Added diagnostic logging for early-return paths and the randomization call (including Time.frameCount).

2. Character bouncing in LobbyScene

Symptom: Character landed and immediately bounced up/down rapidly, indefinitely in lobby.

Root cause — layered diagnosis:

| Layer | Finding |

|---|---|

| Surface | isGrounded was flickering between true and false. |

| Deeper | Ground-check ray started at pivot+0.1 and reached pivot-0.1 — only 0.2 units. |

| Deeper still | The actual ground-contact collider (BodyVisual cone) extended far below the rigidbody pivot, so the raycast never reached the floor in lobby scale. |

| Root cause | BodyVisual was a convex cone with a pointy tip. Single-contact-point physics on a convex hull is inherently unstable — PhysX penetration resolution kept popping the body up, the short ground check lost contact, gravity pulled it back down, repeat. |

Design reconciliation

Discussed with designer: the jellyfish is supposed to *hover* above the ground.

  • Cap/Bowl — top dome (transformable)
  • Trailing body (cone) — main body mass hanging below the cap
  • Tentacles — decorative strands

The whole jellyfish (cap + trailing body) should hover. A separate invisible anchor at pivot Y=0 should be what actually touches the ground.

The current prefab had BodyVisual (trailing body) at Y=0 doing double duty as the ground anchor — that's why its pointy tip was the ground contact, and that's why bouncing happened.

Fix — prefab restructure

Before:

After:

Concrete changes to PlayerPrefab.prefab:

1. Added PlayerAnchor layer at slot 6 in ProjectSettings/TagManager.asset

2. Updated collision matrix in ProjectSettings/DynamicsManager.asset — PlayerAnchor (bit 6) no longer collides with itself. Layer 6's mask = bfffffff (little-endian of 0xFFFFFFBF)

3. Added SphereCollider (r=0.1, center 0,0,0) directly on the root GameObject — the new invisible ground anchor

4. Set root m_Layer: 6 (PlayerAnchor)

5. Renamed BodyVisual → TrailingBody

6. Reparented TrailingBody from root to VisualRoot (inherits pour tilt, form flip, pulse squish, hover bob)

7. Moved TrailingBody to Y=0.8 so it hovers below the cap

8. Deleted VisualBase / VisualEnhanced / VisualTerminal (empty tier placeholders — evolution tier visuals never implemented, handled by color tint in JellyfishVisuals instead)

9. Flipped default active state: Cap IsActive=1, Bowl IsActive=0 (Mobility is default; Carrier engaged by transform input)

Fix — script updates

  • SimpleCharacterController1.cs:
  • [RequireComponent(typeof(MeshCollider))][RequireComponent(typeof(Collider))]
  • private MeshCollider meshColliderprivate Collider mainCollider
  • GetComponent()GetComponent()
  • Removed the _colliderBottomOffset calc — pivot IS the ground anchor now, no offset needed
  • Simplified CheckGroundStatus — cast from transform.position + up*0.15 down by 0.25
  • Kept coyote time (0.15s) and upward-Y clamp when grounded as belt-and-suspenders stability
  • JellyfishVisuals.cs: Removed the explicit bodyVisual.localPosition = up * _currentHoverOffset — TrailingBody is now a child of VisualRoot, which already bobs, so the manual override would double-bob.
  • UnifiedBar.cs: Deleted visualBase/Enhanced/Terminal SerializeFields and the SwapEvolutionVisual() method. Evolution tier is now purely a color-tint effect (no mesh swap).

3. Tooling: OMC plugin update + duplicate hook fix

Symptom: PreToolUse hook from the oh-my-claudecode plugin was injecting "The boulder never stops. Continue until all tasks complete." on every single tool call — Read, Grep, Edit, Bash. Got flagged as a prompt-injection lookalike.

Diagnosis: Plugin was on version 3.9.6. The pre-tool-enforcer.mjs script's toolName lookup table (which should map Read → "Read multiple files in parallel…", etc.) was hitting the fallback branch for every tool, suggesting the Claude Code hook payload schema had changed and 3.9.6 was parsing the new payload wrong.

Fix: Updated OMC plugin 3.9.6 → 4.12.0 via /plugin + /reload-plugins. Confirmed the lookup now resolves correctly — each tool gets its intended coaching reminder instead of the fallback. No local patch needed.

Unrelated: everything-claude-code duplicate hook

/doctor flagged the everything-claude-code plugin registering hooks/hooks.json twice. Root cause: the plugin's .claude-plugin/plugin.json explicitly referenced "hooks": "./hooks/hooks.json", but Claude Code auto-loads that path already — so it loaded twice.

Fix: Removed the hooks entry from the plugin's plugin.json. Reverts on plugin update; upstream fix belongs on their repo.

Files touched

| File | Change |

|---|---|

| Assets/ProtoV2/Scripts/Multiplayer/MultiplayerManager.cs | Awake disables joining immediately |

| Assets/ProtoV2/Scripts/FloorSystem/FloorManager.cs | DeferredSpawn coroutine replaces _pendingSpawn |

| Assets/ProtoV2/Scripts/SpawnManager.cs | Diagnostic logging |

| Assets/ProtoV2/Scripts/CharacterController1.cs | Collider instead of MeshCollider, pivot-based ground check, coyote time |

| Assets/ProtoV2/Scripts/JellyfishVisuals.cs | Removed redundant bodyVisual bob |

| Assets/ProtoV2/Scripts/UnifiedBar.cs | Removed tier visual fields + swap method |

| Assets/ProtoV2/Prefabs/PlayerPrefab.prefab | Tier placeholders deleted, BodyVisual→TrailingBody reparented+renamed, SphereCollider added, layer set, Cap/Bowl active state flipped |

| ProjectSettings/TagManager.asset | Added PlayerAnchor layer at slot 6 |

| ProjectSettings/DynamicsManager.asset | Collision matrix: PlayerAnchor layer no longer self-collides |

| ~/.claude/plugins/.../everything-claude-code/plugin.json | Removed redundant hooks entry |

Open / Follow-ups

  • Test in editor: spawn should now be random on first load AND on reload; lobby character should rest cleanly without bouncing; jellyfish should appear with visible cap + hovering trailing body + tentacles.
  • Optional: Root GameObject still has a MeshFilter component (&7409285671972598714) leftover from when BodyVisual was consolidated. Unknown whether it renders anything — worth inspecting if duplicate rendering still occurs.
  • Upstream issue (everything-claude-code): file a bug so the hooks entry gets removed from the shipped manifest.

date: 2026-04-17

phase: UI Phase 3 — Component Library

status: complete

tags: [ui, component-library, runtime-builder, corner-marks, clinical-button, progress-bar, slider]

---

2026-04-17 — UI Component Library (Phase 3)

Summary

Implemented the full Phase 3 component library for Ritual & Ruin's "dark kawaii terminal" UI aesthetic. All components are runtime-built via static factory methods — no .prefab files created, consistent with the existing SettingsMenuBuilder pattern.

---

Files Created

Sprite Generator (Editor)

  • Assets/ProtoV2/Scripts/Editor/CornerMarkSpriteBuilder.cs
  • MenuItem: Ritual & Ruin/UI/Build Corner Mark Sprite
  • Generates three sprites programmatically and saves to both Assets/ProtoV2/Sprites/UI/ and Assets/ProtoV2/Resources/UI/ (so Resources.Load works at runtime)
  • CornerMark.png — 16×16 L-bracket + diamond pip (white on transparent, FilterMode=Point)
  • BarSegment.png — 8×12 solid white block for segmented progress bars
  • DiamondHandle.png — 18×18 diamond shape (Manhattan-distance fill) for slider handles
  • Idempotent — rerun overwrites

UIBuilder Factory

  • Assets/ProtoV2/Scripts/UI/UIBuilder.cs
  • UIBuilder.AddCornerMarks(RectTransform, Color) — four L-bracket sprites at all corners, tintable, returns Image[4] array
  • UIBuilder.Button(parent, label, onClick) — button with ClinicalButton behaviour + corner marks
  • UIBuilder.Panel(parent, headerText, size) — panel with Terminal Dark bg, Dim Green border, corner marks, optional header strip
  • UIBuilder.ProgressBar(parent, width, height, segments) — segmented fill bar with bracket end caps and ClinicalBar behaviour
  • UIBuilder.Slider(parent, label, initialValue, onChange) — diamond handle slider with ClinicalSlider behaviour
  • UIBuilder.InputField(parent, placeholder) — input field with block cursor and ClinicalInputField behaviour
  • UIBuilder.TabBar(parent, tabLabels, onTabChange) — tab bar with ClinicalTabBar behaviour
  • UIBuilder.Tooltip(parent, text, buttonIcon) — small hint panel with optional icon

Behaviour MonoBehaviours

  • Assets/ProtoV2/Scripts/UI/Components/ClinicalButton.cs — 5-state colour swaps (Default/Hover/Pressed/Disabled/Focus), corner mark brightness, no tweening (Phase 5)
  • Assets/ProtoV2/Scripts/UI/Components/ClinicalBar.cs — semantic colour mapping (green ≥65% / amber 30–65% / red <30%), segment fill updates, end-cap colour
  • Assets/ProtoV2/Scripts/UI/Components/ClinicalSlider.cs — wraps Unity Slider, diamond handle colour on hover, value label update
  • Assets/ProtoV2/Scripts/UI/Components/ClinicalInputField.cs — block cursor blink at 1 Hz, corner marks appear on focus
  • Assets/ProtoV2/Scripts/UI/Components/ClinicalTabBar.cs — label tinting, background swap, underline image repositioned to active tab (direct snap, no animation)

Smoke Test (Editor)

  • Assets/ProtoV2/Scripts/Editor/UIBuilderSmokeTest.cs
  • MenuItem: Ritual & Ruin/UI/Spawn Component Gallery
  • Spawns one of each component on a scratch canvas in the current scene
  • Idempotent — reruns destroy the previous gallery first
  • Does NOT require Play Mode

---

Refactors to Existing Controllers

`MainMenuController.cs`

  • CreateButton() / ApplyButtonVisuals() replaced with a single UIBuilder.Button(...) call
  • Unused colour constants (CardColor, OverlayColor, BtnNormal, BtnHighlight, BtnPressed, LabelColor, BTN_FONT_SIZE) removed
  • All button behaviour (OnClick handlers, labels) unchanged

`PauseController.cs`

  • Same refactor — CreateButton() / ApplyButtonVisuals() replaced with UIBuilder.Button(...)
  • Unused colour constants removed
  • All button behaviour unchanged

Both controllers now get corner marks and ClinicalButton state handling automatically on all three buttons.

---

How to Verify

1. Build sprites first — run Ritual & Ruin/UI/Build Corner Mark Sprite once. Must do this before using the library or spawning the gallery.

2. Build typography — run Ritual & Ruin/Typography/Build Typography Assets if not already done.

3. Spawn the gallery — run Ritual & Ruin/UI/Spawn Component Gallery. A UIGallery_Scratch canvas appears in the scene hierarchy. In Scene view you should see:

  • Three buttons with L-bracket corner marks at all four corners
  • A panel with header strip and corner marks
  • Three progress bars (green/amber/red by value)
  • A slider with diamond handle
  • An input field
  • A four-tab tab bar with underline on the first tab
  • A tooltip hint strip

---

Known Limitations

  • No animation — Phase 5 will add: button press scale punch, hover ink-fill, corner mark breathe, bar overshoot, underline slide
  • No bloom/glow on borders — Outline component approximates the border; true glow requires Phase 4 URP bloom volumes
  • ClinicalButton border colour — not yet driven at runtime (Outline is set once by UIBuilder). Phase 5 will animate this.
  • Sprites require manual build stepResources.Load in UIBuilder falls back gracefully (logs a warning, renders white squares if sprites missing). Run the menu item once and they persist.
  • SettingsMenuBuilder not refactored — its slider/dropdown/toggle widgets are minor variants of UIBuilder components. Marked as a future consolidation opportunity; not done now to avoid scope creep.

---

Adjacent Ideas (Not Implemented)

  • UIBuilder.Toggle() factory — would unify SettingsMenuBuilder's toggle with the library
  • UIBuilder.Dropdown() — would cover the settings dropdowns
  • ClinicalButton Outline colour driven at runtime from state (trivial addition for Phase 5)
  • ClinicalBar danger pulse (opacity oscillation + shake) — spec'd in §5.2, deferred to Phase 5

---

---

date: 2026-04-17

session: UI Phase 4 — Global Visual Layer

status: complete (code + assets shipped; editor wiring still required)

---

Dev Log — UI Phase 4: Global Visual Layer

What shipped

1. CRT Overlay Shader

Assets/ProtoV2/Shaders/CRTOverlay.shader

URP-compatible unlit shader for a full-screen ScreenSpaceOverlay canvas Image. Implements per-spec §4.1 parameters:

  • _ScanlineOpacity (0–1, default 0.25) — darkens every 3rd pixel row
  • _VignetteStrength (0–1, default 0.4) — radial edge darkening
  • _ScreenCurvature (0–0.1, default 0.03) — barrel distortion remapping UVs
  • _ChromaticAberration (0–0.02, default 0.005) — R shifts left, B shifts right
  • _NoiseGrain (0–0.1, default 0.03) — animated per-frame hash grain
  • _Intensity (0–1, default 1.0) — master multiplier; all sub-effects scale by this

Design decision — overlay Canvas vs URP Renderer Feature:

A URP full-screen pass (ScriptableRenderFeature + fullscreen blit) would read back the real framebuffer and apply distortion to actual game pixels. That requires IntermediateTextureMode = Always on the renderer and adds a render pass, which could impact the Obi Fluid Compute backend.

The ScreenSpaceOverlay Canvas Image approach is simpler, zero-cost on the render pipeline, and ships without touching PC_Renderer.asset. The trade-off: because the Image's _MainTex is white (no framebuffer read-back), barrel distortion and chromatic aberration affect the scanline/vignette/grain layers only, not the underlying game image. This is visually correct for the "CRT screen glass" metaphor — the curvature is the screen edge, not a lens distorting the world.

If full framebuffer distortion is needed in a later phase, upgrading to a Renderer Feature is straightforward: the shader is already written to accept _MainTex from a blit.

2. CRT Overlay Controller

Assets/ProtoV2/Scripts/UI/CRTOverlayController.cs

  • Singleton, DontDestroyOnLoad — place one instance in any scene; it survives scene loads.
  • Creates CRTOverlayCanvas (ScreenSpaceOverlay, sort order 200) and a full-screen RawImage with a runtime material instance at Awake().
  • Reads GameSettings.CRTIntensity on Start(), subscribes to GameSettings.OnCRTIntensityChanged.
  • GlitchBurst() / GlitchBurst(float peak, float duration, float chromaBoost) — coroutine that spikes _Intensity and _ChromaticAberration, fires a 50ms black flash, then smoothly restores baseline. API is ready; no caller wiring yet.

3. GameSettings — CRT Intensity Property

Assets/ProtoV2/Scripts/Settings/GameSettings.cs

Added:

  • public static float CRTIntensity { get; private set; } = 1.0f (default per §4.1)
  • public static event System.Action OnCRTIntensityChanged
  • public static void SetCRTIntensity(float v) — clamps, persists to Settings_CRTIntensity, fires event
  • Load/Save wired to Settings_CRTIntensity PlayerPrefs key

4. Settings Menu — CRT Intensity Slider

Assets/ProtoV2/Scripts/Settings/SettingsMenuBuilder.cs

BuildGameplayContent() — replaced the disabled "CRT INTENSITY (coming soon)" toggle row with a fully functional slider row labelled "CRT INTENSITY". Wired to GameSettings.SetCRTIntensity. The (coming soon) label and CanvasGroup alpha=0.4 dim-state are removed.

5. URP Volume Profiles

Assets/Settings/Volumes/ (directory created)

| File | Bloom Intensity | Threshold | Scatter | Tint |

|---|---|---|---|---|

| GameplayBloomProfile.asset | 0.4 | 0.8 | 0.7 | #00FF41 (0,1,0.2549,1) |

| MenuBloomProfile.asset | 0.2 | 0.8 | 0.7 | #00FF41 |

| TerminalPhaseBloomProfile.asset | 0.7 | 0.8 | 0.7 | #FF2222 (1,0.1333,0.1333,1) |

Colour note: #00FF41 in linear sRGB is approximately (0, 1, 0.2549, 1). #FF2222 is (1, 0.1333, 0.1333, 1). The YAML uses linear values as required by Unity's VolumeProfile serialisation.

6. Terminal Phase Bloom Controller

Assets/ProtoV2/Scripts/UI/TerminalPhaseBloomController.cs

  • Two Volume references: _gameplayVolume (stays at weight 1), _terminalVolume (starts at 0).
  • EnterTerminal() — blends _terminalVolume.weight 0→1 over _blendDuration (default 0.3s, SmoothStep).
  • ExitTerminal() — blends weight 1→0.
  • TerminalPhaseController does not currently expose a public event; wire-up is a follow-up editor task (noted below).

7. Reusable UI Effects — API Only

Assets/ProtoV2/Scripts/UI/Effects/GlitchBurst.cs

  • RequireComponent(RectTransform). Burst() / Burst(float durationSeconds).
  • Creates R+B tinted ghost Image clones as siblings, offsets them ±4px on X per §4.4.
  • Fires 50ms black flash, then SmoothStep fade-in over 0.3s.
  • Ghost GOs destroyed on coroutine end — no scene pollution.
  • Handles Image, RawImage, and generic Graphic sources.

Assets/ProtoV2/Scripts/UI/Effects/ScanlineSweep.cs

  • RequireComponent(RectTransform). Reveal() / Reveal(float durationSeconds).
  • Creates a 2px horizontal #00FF41 80% Image child that sweeps top-to-bottom.
  • Each immediate child gets a runtime CanvasGroup (if not already present); alpha starts at 0 and snaps to 1 as the scanline passes.
  • Scanline Image destroyed on coroutine end.

---

Editor-side wiring still required

These steps cannot be done from code without scene modification — they need to be done manually in the Unity Editor.

CRT Overlay Canvas

1. Open MainMenu, LobbyScene, MatchScene.

2. Add an empty GO named CRTOverlayController to each scene.

3. Add the CRTOverlayController component. Assign CRT Shader field → ProtoV2/CRTOverlay.

  • Alternatively leave the field empty; Shader.Find("ProtoV2/CRTOverlay") is the fallback (works in Editor; may fail in stripped builds — prefer inspector assignment).

4. The controller is DontDestroyOnLoad, so only one instance survives scene transitions. Having it in every scene ensures it exists from the first frame regardless of entry scene.

Global Volume — Bloom (MatchScene)

1. Open MatchScene.

2. Add a GO → Volume component, set Profile → GameplayBloomProfile.asset.

3. Set Is Global = true, Weight = 1, Priority = 1 (or above any existing volume).

4. Optionally assign this Volume reference to TerminalPhaseBloomController._gameplayVolume.

Global Volume — Terminal Phase Bloom (MatchScene)

1. Add a second GO → Volume, Profile → TerminalPhaseBloomProfile.asset.

2. Set Is Global = true, Weight = 0 (starts invisible), Priority = 2 (above gameplay volume).

3. Add TerminalPhaseBloomController to any persistent GO in the scene.

4. Assign Gameplay Volume and Terminal Volume references in inspector.

5. Wire TerminalPhaseBloomController.EnterTerminal() to the appropriate moment — currently suggest calling it from TerminalPhaseController.EnterTerminal(GameObject) after the aura is attached. A future session should add a public static event Action OnTerminalPhaseEntered to TerminalPhaseController.

Global Volume — Bloom (MainMenu, LobbyScene)

1. Add a GO → Volume → Profile → MenuBloomProfile.asset.

2. Is Global = true, Weight = 1.

CRT Shader — Include in Build

1. Window → Rendering → Shader Inclusion → add ProtoV2/CRTOverlay to the Always Included Shaders list (or ensure a material referencing it exists and is in a Resources folder). The runtime Shader.Find() fallback relies on this.

---

Known limitations

  • Barrel distortion does not warp game pixels — only the CRT overlay layers (scanlines, vignette, grain) are distorted. The underlying game image is undistorted. This is acceptable for the phosphor-glass metaphor and avoids a renderer feature dependency. Upgrade path documented in shader comments.
  • TerminalPhaseController eventEnterTerminal() on TerminalPhaseBloomController is not called automatically yet. Needs a one-line hook in TerminalPhaseController.EnterTerminal(GameObject) or a public static event.
  • GlitchBurst / ScanlineSweep — no callers yet. Phase 5 wires them to transitions.
  • CRTOverlayController DontDestroyOnLoad — if the game re-enters a scene that already spawned the controller on a previous load, the duplicate will self-destroy via the singleton guard. This is correct behaviour.
  • Volume profile colour values — stored in YAML as linear floats, not gamma hex. #00FF41 → (0, 1, 0.2549, 1) and #FF2222 → (1, 0.1333, 0.1333, 1). If Unity re-imports and shows different values, use the Color Picker in the Volume inspector to correct to the hex target.

---

Files created / modified

| Action | Path |

|---|---|

| NEW | Assets/ProtoV2/Shaders/CRTOverlay.shader |

| NEW | Assets/ProtoV2/Scripts/UI/CRTOverlayController.cs |

| NEW | Assets/ProtoV2/Scripts/UI/TerminalPhaseBloomController.cs |

| NEW | Assets/ProtoV2/Scripts/UI/Effects/GlitchBurst.cs |

| NEW | Assets/ProtoV2/Scripts/UI/Effects/ScanlineSweep.cs |

| NEW | Assets/Settings/Volumes/GameplayBloomProfile.asset |

| NEW | Assets/Settings/Volumes/MenuBloomProfile.asset |

| NEW | Assets/Settings/Volumes/TerminalPhaseBloomProfile.asset |

| MODIFIED | Assets/ProtoV2/Scripts/Settings/GameSettings.cs — added CRTIntensity, OnCRTIntensityChanged, SetCRTIntensity |

| MODIFIED | Assets/ProtoV2/Scripts/Settings/SettingsMenuBuilder.cs — CRT row now a functional slider |

---

---

date: 2026-04-17

session: UI Text Swap — Phase 1 (clinical language pass) + Environmental-storytelling UI doc alignment

status: complete

refs:

  • "Games/Ritual & Ruin/Options/UI Implementation Checklist.md"
  • "Games/Ritual & Ruin/Confirmed/UI Visual Guidelines.md"
  • "Decision Log/UI Copy Phase 1 Decision 2026-04-17.md"
  • "Decision Log/Story Mode vs Arcade Mode UI Decision.md"
  • "Games/Ritual & Ruin/Options/Team Identifier — In-World Fiction.md"
  • "Games/Ritual & Ruin/Options/Experimental UI Framing.md"
  • "Games/Ritual & Ruin/UI Design/CRT Filter Toggle.md"

---

UI Text Swap — Phase 1 + Environmental-Storytelling Doc Alignment

Session had two halves:

1. Design-doc alignment — re-read every UI-related design doc, caught stale direction (the Pill Selector entry concept was still live in the Options docs despite being superseded by the 5 April Decision Log entry). Wrote a new Decision Log entry consolidating every copy decision made today and a standalone exploration doc for the one remaining blocker (the team identifier).

2. Phase 1 code swap — pure copy-and-label pass on existing menus to bring them in line with the clinical "experiment monitoring system" tone from the UI Visual Guidelines. No scene YAML edits, no new prefabs, no refactoring. Pure .cs changes only.

---

Files Edited

`Assets/ProtoV2/Scripts/MainMenuController.cs`

| Before | After |

|--------|-------|

| "local multiplayer arena" (subtitle) | "SURVIVAL IS MEASURED, NOT CELEBRATED" |

| "PLAY" (start button) | "[ INITIATE EXPERIMENT ]" |

| "SETTINGS" (settings button) | "[ CONFIGURE PARAMETERS ]" |

| "QUIT" (quit button) | "[ TERMINATE SESSION ]" |

Title "RITUAL & RUIN" unchanged — was already correct.

`Assets/ProtoV2/Scripts/PauseController.cs`

| Before | After |

|--------|-------|

| "PAUSED" (title) | "EXPERIMENT SUSPENDED" |

| "RESUME" | "[ RESUME EXPERIMENT ]" |

| "SETTINGS" | "[ CONFIGURE PARAMETERS ]" |

| "QUIT TO MENU" | "[ ABORT EXPERIMENT ]" |

`Assets/ProtoV2/Scripts/Settings/SettingsMenuBuilder.cs`

  • Back button: "BACK""[ CLOSE ]"
  • Section labels (all BuildRow call strings):

| Before | After |

|--------|-------|

| "Master Volume" | "MASTER VOLUME" |

| "Music Volume" | "MUSIC VOLUME" |

| "SFX Volume" | "SFX VOLUME" |

| "Window Mode" | "WINDOW MODE" |

| "Resolution" | "RESOLUTION" |

| "Max FPS" | "FRAME RATE CAP" |

| "VSync" | "V-SYNC" |

| "Quality Preset" | "QUALITY PRESET" |

| "Anti-Aliasing" | "ANTI-ALIASING" |

| "Bloom" | "BLOOM" |

| "Shadows" | "SHADOW QUALITY" |

| "Camera Shake" | "CAMERA SHAKE" |

| "CRT Effect (coming soon)" | "CRT INTENSITY (coming soon)" |

Title "SETTINGS" unchanged — per spec, no bracket wrap on titles.

`Assets/ProtoV2/Scripts/Editor/InputRebindMenuSetup.cs`

| Before | After |

|--------|-------|

| $"PLAYER {playerNum}" | $"SUBJECT {playerNum:D2}" (→ SUBJECT 01…04) |

| "START GAME" (start button) | "[ COMMENCE EXPERIMENT ]" |

| "Press ENTER (Keyboard) or START/A (Controller) to join\n…" | "PRESS ENTER OR START/A TO ASSIGN SUBJECT\n…" |

`Assets/ProtoV2/Scripts/UI/InputRebindMenuUI.cs`

| Before | After |

|--------|-------|

| "START GAME" (runtime can-start text) | "[ COMMENCE EXPERIMENT ]" |

| $"NEED {minPlayersToStart} PLAYER(S)" | $"AWAITING {minPlayersToStart} SUBJECT(S)" |

| "Press ENTER…" instructions | "PRESS ENTER OR START/A TO ASSIGN SUBJECT\n…" |

`Assets/ProtoV2/Scripts/MatchCountdown.cs`

| Before | After |

|--------|-------|

| i.ToString() (count beat display) | $"EXPERIMENT INITIATING IN {i}" |

| "GO!" | "EXPERIMENT INITIATED" |

---

New File: `Assets/ProtoV2/Scripts/UI/MatchTimerUI.cs`

New MonoBehaviour component that displays a match elapsed-time timer in the top-right corner of the HUD.

Wiring:

  • Subscribes to MatchCountdown.Instance.OnCountdownComplete to start ticking (resets elapsed to 0).
  • Subscribes to MatchManager.Instance.OnMatchEnd to stop ticking.
  • If the Inspector timerText field is left null (the default for a new scene object), Start() calls CreateTimerElement() which bootstraps its own ScreenSpaceOverlay Canvas (sortingOrder 80, matching the Countdown Canvas layer from spec §8) with a top-right anchored TextMeshProUGUI.

Format: M:SS.mmm (e.g. 0:04.217, 1:32.005) — matches UI Screen Specs §"Formatting Rules".

How to add to MatchScene: Add an empty GameObject to MatchScene, attach the MatchTimerUI component. No Inspector wiring required — it bootstraps the canvas and label at runtime. Wire timerText manually in a later phase if you want it to live inside the existing HUD Canvas hierarchy.

---

What Was Deferred

  • TMP element not added to MatchScene YAML — the component self-bootstraps at runtime, so no scene YAML edit was needed. When you want to move the timer into the existing HUD Canvas (Phase 6 restructure), wire the timerText Inspector field to a pre-existing TMP object and the bootstrap path is skipped.
  • [ RECALIBRATE INPUT ] button in PauseController — checklist item 1.2 lists an "Input config button" for the pause menu. No such button exists in the current PauseController.cs BuildUI() — there are only three buttons (Resume, Settings, Quit). Not added: out of scope for this session and the InputRebindMenuUI flow is lobby-only. Flagged here for a future pass.
  • [ RESTORE DEFAULTS ] / [ APPLY ] footer buttons in SettingsMenuBuilder — checklist item 1.3. Current builder has only a single "back" button footer; no Apply/Restore Defaults buttons exist yet. Left untouched — structural change, not a copy swap.
  • Phase 2 typography (Share Tech Mono, VT323 fonts, Phosphor Green colour) — deferred per plan.
  • End screen copy — blocked on team identifier decision per checklist.
  • Countdown team banners — blocked on team identifier decision.
  • OmniSharp LSP not installed in this environment — compile-error verification was done by manual read of the final file state. No new compile errors expected: all changes are string literals or event subscription patterns that match existing codebase conventions exactly.

---

Checklist Items Completed (from UI Implementation Checklist.md)

  • [x] 1.1 Add tagline below title → SURVIVAL IS MEASURED, NOT CELEBRATED
  • [x] 1.1 Start button → [ INITIATE EXPERIMENT ]
  • [x] 1.1 Settings button → [ CONFIGURE PARAMETERS ]
  • [x] 1.1 Quit button → [ TERMINATE SESSION ]
  • [x] 1.2 Pause title → EXPERIMENT SUSPENDED
  • [x] 1.2 Resume button → [ RESUME EXPERIMENT ]
  • [x] 1.2 Settings button → [ CONFIGURE PARAMETERS ]
  • [x] 1.2 Quit to menu button → [ ABORT EXPERIMENT ]
  • [x] 1.3 Section labels all-caps + wording updates (13 labels)
  • [x] 1.3 Back/close button → [ CLOSE ]
  • [x] 1.5 Slot heading → SUBJECT 01SUBJECT 04
  • [x] 1.5 Start button → [ COMMENCE EXPERIMENT ]
  • [x] 1.6 Countdown sequence → EXPERIMENT INITIATING IN 3/2/1EXPERIMENT INITIATED
  • [x] Match elapsed timer (top-right) — new MatchTimerUI.cs, runtime bootstrap

---

Documentation Reconciliation

Side track to the code work. Design docs were out of sync with the 5 April 2026 decision in [[Decision Log/Story Mode vs Arcade Mode UI Decision]] — the Pill Selector entry concept and the dual warm/cold aesthetic had been rejected, but the source docs still read as if both were live. Brought them in line and captured today's new decisions in a single authoritative Decision Log entry so future sessions don't have to re-derive the state.

Docs Written or Updated

| Path | Operation | What changed |

|------|-----------|--------------|

| Decision Log/UI Copy Phase 1 Decision 2026-04-17.md | New | Authoritative consolidation of every copy and structural decision made in this session — menu labels, pause copy, settings labels, lobby copy, countdown flow, match timer format, CRT scope correction, pill-selector reaffirmation. Lists what's pending (team identifier) and what was deliberately deferred (typography, component styling, animation, HUD overlay conversion). |

| Games/Ritual & Ruin/Options/Team Identifier — In-World Fiction.md | New | Workspace doc for the blocker. Frames the three open questions (why teams, why team-healing, what the label should make the player feel), candidate-labels scratchpad with fiction-implied-by-label analysis, constraints checklist, tie-ins to existing lore/systems, and a suggested resolution path. Intended as the author's working surface — fill-in-the-blanks sections under "Working Notes". |

| Games/Ritual & Ruin/Options/UI Implementation Checklist.md | Updated | Phase 1.1–1.3 and the unblocked parts of 1.5/1.6 marked complete. 1.4 (End Screen) and 1.7 (Match Manager elimination copy) marked BLOCKED with link to the Team Identifier doc. Added the 2026-04-17 countdown+timer decision to the Pending Decisions table. Status-snapshot table refreshed. |

| Games/Ritual & Ruin/Options/Experimental UI Framing.md | Updated | Status changed from exploring to superseded. Added a prominent red supersession banner at the top listing what survives (clinical narrative voice, tension-point inventory, diegetic UI principle) vs. what's rejected (Pill Selector entry, dual-style, CRT-as-mode-switch). Kept historical content below for context. Explicit "Do not implement from it" instruction. |

| Games/Ritual & Ruin/UI Design/CRT Filter Toggle.md | Updated | Rewritten. Removed pill-icon-slider metaphor (deprecated). Repositioned CRT as a settings-only accessibility slider (0–100% scaling of scanline/vignette/curvature/chromatic aberration/noise together). References UI Visual Guidelines §4.1 for the intensity values. Added Deprecated Historical Note pointing to the rejected direction for future readers. |

Why This Matters Going Forward

  • The 2026-04-17 Decision Log entry is now the single point of truth for all Phase 1 copy. If any downstream copy question comes up ("what does the pause button say?", "what's the countdown final beat?"), that file answers it. No need to reconstruct from scattered Options docs.
  • The Team Identifier doc turns a nebulous blocker ("we don't have team copy yet") into a structured design problem with questions, candidates, and constraints. The next working session on that topic has a clear entry point.
  • The superseded banners on the pill-selector docs prevent future contributors (or future-me) from re-implementing a rejected concept because it was still labelled exploring.

Checklist Status After Session

| Checklist section | State |

|-------------------|-------|

| 1.1 Main Menu | Complete (version number bottom-right deferred) |

| 1.2 Pause Menu | Complete (Recalibrate Input button deferred — structural) |

| 1.3 Settings Modal | Complete (Restore Defaults / Apply footer deferred — structural; typography deferred to Phase 2) |

| 1.4 End Screen | BLOCKED on team identifier |

| 1.5 Lobby | Core labels complete; team divider blocked on team identifier |

| 1.6 Match Countdown | Text flow complete + plain top-right timer shipped; animation and team banners deferred |

| 1.7 Match Manager elimination | BLOCKED on team identifier |

| Phase 2 onward | Not started |

---

Next Session Candidates

Pick one:

  • Unblock team identifier — work through Options/Team Identifier — In-World Fiction.md. Once resolved, ship the ~3-file Phase 1.b copy pass (EndScreen, MatchManager, Countdown banners, lobby divider) in a short follow-up.
  • Phase 2 typography — import Share Tech Mono + VT323, create TMP font assets with phosphor glow material, apply across all existing UI text. No blockers, purely additive.
  • Phase 6 HUD overlay conversion — the biggest visual shift. Follow Options/Match HUD Overlay Design.md. This is where the "environmental storytelling" language of the UI actually lands at the match scene level.

---

---

title: UI Typography — Phase 2

date: 2026-04-17

session: Phase 2 — Typography and Colour Application

status: complete (pending one manual editor step — see below)

tags: [ritual-ruin, ui, typography, tmp, fonts, phosphor-green]

---

UI Typography Phase 2 — Session Log

Summary

Implemented Phase 2 of the UI Implementation Checklist: Share Tech Mono and VT323 are now the game's fonts, backed by a central TypographyLibrary ScriptableObject. All runtime-code-created TMP components in the priority controllers have been patched to pull from this library. A one-shot editor script builds the TMP font assets and seven TMP materials.

---

Files Created

Fonts

| File | Notes |

|------|-------|

| Assets/ProtoV2/Fonts/ShareTechMono-Regular.ttf | Downloaded from github.com/google/fonts (OFL) |

| Assets/ProtoV2/Fonts/VT323-Regular.ttf | Downloaded from github.com/google/fonts (OFL) |

| Assets/ProtoV2/Fonts/ShareTechMono-OFL.txt | SIL Open Font Licence — required by OFL terms |

| Assets/ProtoV2/Fonts/VT323-OFL.txt | SIL Open Font Licence — required by OFL terms |

TMP font assets (ShareTechMono SDF.asset, VT323 SDF.asset) are generated by the editor script below — they do not exist yet and will appear at Assets/ProtoV2/Fonts/ after running the menu item.

Scripts

| File | Notes |

|------|-------|

| Assets/ProtoV2/Scripts/Typography/TypographyLibrary.cs | ScriptableObject — central font/material/colour registry. Loaded at runtime via Resources.Load("TypographyLibrary"). |

| Assets/ProtoV2/Scripts/Editor/TMPFontAssetBuilder.cs | One-shot editor script. Builds TMP font assets, seven TMP materials, and the TypographyLibrary asset. MenuItem: Ritual & Ruin / Typography / Build Typography Assets. |

Materials (created by editor script — not yet on disk)

Seven materials will be saved to Assets/ProtoV2/Materials/UI/ after running the build menu item:

| Material | Face colour | Glow |

|----------|-------------|------|

| TMP_PhosphorGreen.mat | #00FF41 | yes, 0.6 power |

| TMP_BrightPhosphor.mat | #39FF14 | yes, 0.6 power |

| TMP_MidGreen.mat | #008F11 | none |

| TMP_DimGreen.mat | #004D00 | none |

| TMP_BloodRed.mat | #CC0000 | yes, 0.4 power |

| TMP_AcidGreen.mat | #7FFF00 | yes, 0.8 power |

| TMP_VT323_Phosphor.mat | #00FF41 | yes, VT323 base font |

ScriptableObject (created by editor script)

Assets/ProtoV2/Resources/TypographyLibrary.asset — wired with all font assets and materials. Lives in Resources/ so Resources.Load works at runtime without Inspector wiring on individual controllers.

---

Files Edited

All colour palette changes follow UI Visual Guidelines §2.1. All font assignments go through TypographyLibrary.

| File | What changed |

|------|-------------|

| Assets/ProtoV2/Scripts/MainMenuController.cs | Replaced gold/lavender palette with phosphor-green palette. Title uses VT323 (display size) with glow. Subtitle uses VT323 Mid Green. Buttons use Share Tech Mono with PhosphorGreen glow material. |

| Assets/ProtoV2/Scripts/PauseController.cs | Same palette swap. Title uses Share Tech Mono headline. Buttons Share Tech Mono with glow. Separator now Dim Green border colour. |

| Assets/ProtoV2/Scripts/Settings/SettingsMenuBuilder.cs | Palette swap throughout. Tab labels Share Tech Mono. Active tab label PhosphorGreen, inactive MidGreen. All row labels, dropdown labels, value labels Share Tech Mono. Slider fill PhosphorGreen, handle BrightPhosphor. |

| Assets/ProtoV2/Scripts/UI/MatchTimerUI.cs | Timer text colour changed from Color.white to TypographyLibrary.PhosphorGreen. Share Tech Mono + glow material applied in CreateTimerElement(). |

| Assets/ProtoV2/Scripts/UI/EndScreenController.cs | resultText gets Share Tech Mono + PhosphorGreen in Start(). timeText gets VT323 + MidGreen (log-entry style). |

| Assets/ProtoV2/Scripts/MatchManager.cs | outlastTimerText and eliminatedTeamLabel get Share Tech Mono + PhosphorGreen applied in Start(). |

---

Required Manual Step — MUST DO ONCE

Run Ritual & Ruin / Typography / Build Typography Assets from the Unity Editor menu.

This creates:

  • ShareTechMono SDF.asset and VT323 SDF.asset in Assets/ProtoV2/Fonts/
  • Seven .mat files in Assets/ProtoV2/Materials/UI/
  • TypographyLibrary.asset in Assets/ProtoV2/Resources/

Until this runs, the game will log [TypographyLibrary] Asset not found warnings and fall back to TMP's default font — the game still runs correctly, just without the new fonts applied.

The menu item is idempotent — safe to re-run if assets need to be regenerated.

---

Deferred Items (Flagged, Not Done)

| Item | Reason deferred |

|------|----------------|

| Scene YAML TMP font asset references | Runtime code overwrites them anyway; patching YAML is fragile |

| World-space bar colours on UnifiedBar.cs | Phase 6 HUD overlay rewrites this layer; semantic colours (green/amber/red health) intentionally left untouched |

| TMP font fallback chains | Nice-to-have, not critical for Phase 2 |

| Corner-mark sprites | Phase 3 prefab library |

| InputRebindMenuSetup.cs / InputRebindMenuUI.cs font patch | These set up scene hierarchy in editor — font assignment at scene-build time; will be addressed when the rebind prefab is rebuilt in Phase 3 |

| MatchCountdown.cs — countdown text font | countdownText is a serialized Inspector field wired in the scene; runtime font assignment from TypographyLibrary not added as it would fight the YAML ref. Address in Phase 3 or via a small Start() patch when the countdown canvas is rebuilt. |

---

How to Verify in Play Mode

After running the build menu item once:

1. Main Menu: Title "RITUAL & RUIN" renders in VT323 with phosphor-green glow. Subtitle in VT323 mid-green. Buttons in Share Tech Mono bright green.

2. Pause Menu: "EXPERIMENT SUSPENDED" in Share Tech Mono phosphor green. Buttons in Share Tech Mono.

3. Settings Panel: All labels, dropdowns, tab bars in Share Tech Mono phosphor green. Slider fill is #00FF41, handle is #39FF14.

4. Match Timer (top-right): Renders in Share Tech Mono phosphor green.

5. End Screen result/time text: Result in Share Tech Mono PhosphorGreen; time line in VT323 MidGreen.

6. Match Manager outlast labels: Share Tech Mono PhosphorGreen.

If fonts still show as default TMP font after the menu item runs, check the Console for [TypographyLibrary] errors — the most likely cause is a missing TTF at the expected path.

---

---

date: 2026-04-18

session: O7 Pending Wiring — Implementation

status: complete

refs:

  • ".omc/plans/O7-wiring-implementation.md"
  • "Games/Ritual & Ruin/Options/UI Decisions Register.md"
  • "Games/Ritual & Ruin/Options/Match HUD Overlay Design.md"

---

2026-04-18 — O7 Wiring Implementation

Summary

Wired existing FloatingFeedbackSpawner and SceneTransition.Fade primitives into gameplay callsites; added CanvasGroupFade for settings panels; retired FloatingRewardIndicator legacy path; fixed SceneTransition DDOL canvas promotion bug; closed UI Decisions O4/O5/O6/O7.

Session Flow

Used APSU (Opus analyse → Opus plan → 4-executor swarm). The analyst mapped all live callsites and locked public APIs before planning began; the planner merged T1 and T5 into a single track to avoid concurrent edits to MainMenuController.cs and PauseController.cs. Wave 1 ran T1, T2, and T3 in parallel; Wave 2 ran T4 gated on T3 completion; Wave 3 was an orchestrator-driven validation sweep; Wave 4 (this entry) is the docs executor.

Files Changed

New

  • Assets/ProtoV2/Scripts/UI/Animation/CanvasGroupFade.cs
  • Assets/ProtoV2/Scripts/BloodSystem/AltarCompletionFeedback.cs

Modified

  • Assets/ProtoV2/Scripts/UI/Animation/SceneTransition.cs — DDOL canvas-promotion bug fixed; always instantiates a dedicated SceneTransitionCanvas GO (ScreenSpaceOverlay, sortOrder 9999), DDOLs only that GO
  • Assets/ProtoV2/Scripts/MainMenuController.cs — Start button routes through SceneTransition.Fade; settings open/close routes through CanvasGroupFade
  • Assets/ProtoV2/Scripts/PauseController.cs — all scene-load paths route through Fade; settings open/close through CanvasGroupFade
  • Assets/ProtoV2/Scripts/UI/EndScreenController.cs — Rematch + Lobby buttons route through Fade
  • Assets/ProtoV2/Scripts/LobbySceneLoader.cs — match-load method routes through Fade
  • Assets/ProtoV2/Scripts/Settings/SettingsMenuBuilder.csCanvasGroupFade wired on built panel root for fade-in/out hooks
  • Assets/ProtoV2/Scripts/ProgressionManager.csHandleAltarConsumed uses AddBarUnitsSilent + SpawnProximity (no double-floater)
  • Assets/ProtoV2/Scripts/UnifiedBar.cs — added AddBarUnitsSilent(float); removed floatingRewardPrefab serialized field and internal spawn calls
  • Assets/ProtoV2/Scripts/Editor/AlphaSetupWizard.csCreateFloatingRewardPrefab region removed; all FloatingRewardIndicator references deleted
  • Assets/ProtoV2/Prefabs/FloorSystem/Altar.prefabAltarCompletionFeedback component added
  • Assets/ProtoV2/Prefabs/PlayerPrefab.prefab — orphan floatingRewardPrefab: YAML line on UnifiedBar component removed

Deleted

  • Assets/ProtoV2/Scripts/UI/FloatingRewardIndicator.cs
  • Assets/ProtoV2/Prefabs/UI/FloatingRewardIndicator.prefab

Key Decisions

  • DDOL canvas fix: SceneTransition previously promoted whichever canvas it found in the scene to DDOL, causing cross-scene canvas duplication. The fix always creates a fresh SceneTransitionCanvas GO and DDOLs only that — no scene canvas is ever touched.
  • AddBarUnitsSilent to avoid double-spawn: The proximity reward path called the old AddBarUnits which also triggered an internal floater spawn, producing a duplicate alongside SpawnProximity. The new silent variant applies the bar mutation only; ProgressionManager owns the floater call explicitly.
  • Settings fade via CanvasGroupFade: Rather than animate alpha in SettingsMenuBuilder directly, a standalone CanvasGroupFade MonoBehaviour on the panel root is wired by callers. Uses Time.unscaledDeltaTime so it survives pause. 0.15s fade-in / 0.2s fade-out.

Validation

  • refresh_unity + read_console: compile clean, zero errors, zero warnings on touched files.
  • lsp_diagnostics_directory on Assets/ProtoV2/Scripts/: clean.
  • Full menu→match→pause→settings→main cycle: no NullReferenceException / MissingReferenceException.
  • Altar consume: SpawnAltarCompletion floater visible at altar position; SpawnProximity floater visible above winning creature; no duplicate reward floater.
  • grep -R "floatingRewardPrefab" Assets/ProtoV2/: zero hits.
  • grep -Rn "FloatingRewardIndicator" Assets/ProtoV2/Scripts/: zero non-comment hits.
  • MainMenu renders all 3 buttons ([ INITIATE EXPERIMENT ], [ CONFIGURE PARAMETERS ], [ TERMINATE SESSION ]) with symmetric corner marks (screenshot verified).

Open Follow-ups

  • DevPanel.cs scene-reload wrap (dev-only, deferred — not part of this track's scope).
  • Team identifier fiction still pending — O1/H1 remains Provisional; revisit after next playtesting session.

---

---

date: 2026-04-18

session: UI Animation — Phase 5

status: complete

---

UI Animation Phase 5 — Anime-Weight Transitions

Files Created

| File | Purpose |

|------|---------|

| Assets/ProtoV2/Scripts/UI/Animation/Easing.cs | Static easing curves: EaseOutCubic, EaseInCubic, EaseOutBack, EaseInOutExpo, Linear |

| Assets/ProtoV2/Scripts/UI/Animation/UITween.cs | Coroutine tween helpers: TweenFloat, TweenColor, TweenScale, TweenAnchoredPosition, TweenAlpha. All use Time.unscaledDeltaTime. |

| Assets/ProtoV2/Scripts/UI/Animation/PanelEntrance.cs | OnEnable: slide Y from -8px to 0 over 0.3s ease-out back + triggers ScanlineSweep.Reveal() if present |

| Assets/ProtoV2/Scripts/UI/Animation/ButtonGroupStagger.cs | OnEnable: each direct child slides in from left -12px + fades 0→1, staggered 0.08s each, ease-out cubic |

| Assets/ProtoV2/Scripts/UI/Animation/CornerMarkBreathe.cs | Oscillates corner mark scale ±1% at 0.5 Hz ambient breathing |

| Assets/ProtoV2/Scripts/UI/Animation/SceneTransition.cs | SceneTransition.Fade(duration, callback) — glitch burst + black flash + fade in. API only, no auto-wiring. |

| Assets/ProtoV2/Scripts/UI/Animation/FloatingFeedback.cs | Anime-style pop/float/fade for reward/altar/proximity/death/evolution feedback text |

| Assets/ProtoV2/Scripts/UI/Animation/FloatingFeedbackSpawner.cs | Static factory: SpawnReward, SpawnAltarCompletion, SpawnProximity, SpawnDeath, SpawnEvolution |

Files Edited

| File | Change |

|------|--------|

| Assets/ProtoV2/Scripts/UI/Components/ClinicalButton.cs | Full animation layer: hover 0.1s fade, exit 0.15s, press scale punch 1.0→1.04→0.97→1.0, focus gamepad pulse 0.5 Hz |

| Assets/ProtoV2/Scripts/UI/Components/ClinicalBar.cs | Gain overshoot +2 segments + settle, immediate loss drop, danger pulse/shake <30%, PlayCompleteBurst() public method |

| Assets/ProtoV2/Scripts/UnifiedBar.cs | SpawnReward wired at AddBarUnits callsite; SpawnEvolution wired in TriggerEvolution |

| Assets/ProtoV2/Scripts/DeathHandler.cs | SpawnDeath wired in HandleDeath |

| Assets/ProtoV2/Scripts/PauseController.cs | ButtonGroupStagger added to button column; PanelEntrance added to card |

| Assets/ProtoV2/Scripts/MainMenuController.cs | ButtonGroupStagger added to button column; PanelEntrance added to main panel |

| Assets/ProtoV2/Scripts/UI/EndScreenController.cs | DataLoadReveal coroutine: lines appear 0.05s apart (§6.1 data-load pattern) |

Callsites Wired

| Spawner | Callsite | File |

|---------|----------|------|

| SpawnReward | UnifiedBar.AddBarUnits (replaces FloatingRewardIndicator instantiation) | UnifiedBar.cs |

| SpawnEvolution | UnifiedBar.TriggerEvolution (after OnEvolution event) | UnifiedBar.cs |

| SpawnDeath | DeathHandler.HandleDeath (after deathFeedback) | DeathHandler.cs |

| SpawnAltarCompletion | NOT WIRED — follow-up | see below |

| SpawnProximity | NOT WIRED — follow-up | see below |

Callsites NOT Wired (Follow-up)

  • SpawnAltarCompletion: ProgressionManager.HandleAltarConsumed would be the callsite. Proximity reward of 50 should call SpawnProximity; the per-particle batch reward uses SpawnReward (already wired). Deferred because HandleAltarConsumed currently awards raw bar units with no world position feedback — requires a second pass to split the two reward types.
  • SpawnProximity: Same callsite as above (ProgressionManager proximity bonus). Follow-up: modify HandleAltarConsumed to call FloatingFeedbackSpawner.SpawnProximity(closest.transform.position + Vector3.up * 2f, 50).
  • SceneTransition.Fade: No scene load callsites wired. Manual wiring needed at:
  • MatchCountdown.cs: after countdown complete beat, before FloorManager.ActivateCurrentFloors()
  • MainMenuController.OnStartClicked: wrap SceneManager.LoadScene("LobbyScene") in SceneTransition.Fade(0.3f, () => SceneManager.LoadScene("LobbyScene"))
  • PauseController.OnQuitClicked: same pattern for MainMenu load
  • ClinicalInputField cursor blink: Verified at 1 Hz (0.5s interval in existing code) — matches §6.3 spec. No change needed.
  • PanelEntrance on countdown panel / end screen panel: These panels are controlled by SetActive in MatchCountdown and EndScreenController. PanelEntrance.OnEnable fires automatically when the panel is activated — no additional wiring needed as long as PanelEntrance is added to those panel GOs in the scene (manual Inspector step).
  • SettingsMenuBuilder fade-through-black open/close: The settings panel is shown/hidden via SetActive in PauseController and MainMenuController. Phase 5 spec calls for 0.2s fade-out / 0.15s fade-in. Not wired — requires a settings panel wrapper coroutine. Deferred.

Manual Test Checklist (Play Mode)

  • [ ] Button hover: background fades from dark to #003B00 over ~0.1s; corner marks enlarge and brighten
  • [ ] Button hover exit: slightly slower than enter (~0.15s)
  • [ ] Button press: scale punches 1.04→0.97→1.0; background flashes full green; text inverts to black
  • [ ] Gamepad select (navigate with controller): corner marks pulse at ~0.5 Hz while selected
  • [ ] Bar gain (add blood units): fill overshoots by ~2 segments then settles back
  • [ ] Bar loss (decay/damage): fill drops immediately, no bounce
  • [ ] Bar danger (<30%): opacity pulses 1.0→0.55 at ~1.5 Hz; bar shakes ±2px horizontally
  • [ ] Bar danger exit (bar rises above 30%): pulse and shake stop, opacity and position restored
  • [ ] Main menu load: button column slides in from left, staggered ~0.08s per button
  • [ ] Main menu panel: drops in from -8px with slight overshoot
  • [ ] Pause menu open: card drops in; buttons stagger in from left
  • [ ] End screen: result text and time text appear sequentially ~0.05s apart
  • [ ] Death: "ELIMINATED" glitch-appears above creature, holds at scale 1.1, fades in place over 2s
  • [ ] Reward (+N): pops 0→1.2→1 scale, floats up, fades over 1.2s
  • [ ] Evolution: two glitch flashes, "STAGE N INITIATED" punches in, holds 2s, glitch-disappears
  • [ ] All animations run correctly when game is paused (Time.timeScale = 0) — pause menu buttons animate

Notes

  • FloatingRewardIndicator.cs is NOT removed — it's still referenced by UnifiedBar.floatingRewardPrefab SerializedField and by AlphaSetupWizard. The new spawner replaces its instantiation but the prefab and class remain. Safe to remove both in a future cleanup pass.
  • ScanlineSweep.cs (Phase 4) uses Time.deltaTime — pre-existing, not changed. It's called by PanelEntrance which handles its own unscaled timing separately. ScanlineSweep will pause during timeScale=0. Low priority fix since scanline is visual-only.
  • CornerMarkBreathe must be manually added to panels in the scene (or added programmatically in UIBuilder.Panel() when a breathe flag overload is added — deferred).

---

---

date: 2026-04-18

session: UI Animation Refactor — Coroutine → Update

status: complete

refs:

  • "Games/Ritual & Ruin/Options/UI Implementation Checklist.md"
  • "Games/Ritual & Ruin/Confirmed/UI Visual Guidelines.md"
  • ".omc/plans/ui-animation-update-refactor.md"

---

UI Animation Refactor — Coroutine → Update

Goal

Convert every coroutine-based UI animation in Assets/ProtoV2/Scripts/UI/ to Update-tick MonoBehaviour state machines using Time.unscaledDeltaTime. Driven by a real bug encountered earlier today: buttons 2 and 3 were permanently invisible after entering the main menu because child coroutines in the old ButtonGroupStagger died mid-delay when Unity killed them during parent GameObject disable cycles. Coroutine lifecycle is opaque and leads to these "silent state loss" bugs; Update-tick state machines are deterministic and inspectable.

Process

Ran the APSU skill (Analyze → Plan → Swarm + Unity MCP):

1. Opus analyst surveyed 16 files, inventoried every StartCoroutine call, identified state-machine shapes per component, locked public API signatures, flagged a latent ghost-leak bug in GlitchBurst and a Time.deltaTime bug in ScanlineSweep.

2. Opus planner split the work into 5 disjoint-file tracks (A–E) with a single dependency gate (A's UITween.cs delete blocked by D's ClinicalButton.cs rewrite).

3. Swarm — 5 executors in parallel. Sonnet for Tracks A, B, E (medium/low complexity). Opus for Tracks C, D (high complexity — 5 variant state machines and 5-channel tween struct respectively).

Tracks Completed

Track A — Animation Primitives (sonnet)

  • ButtonGroupStagger.cs — single-clock Update, per-child alpha = EaseOutCubic((elapsed − i·0.08) / 0.22), snap to 1 at _elapsed ≥ total
  • PanelEntrance.cs — Y-drop from -8px over 0.3s with EaseOutBack, fires _sweep?.Reveal() at completion
  • SceneTransition.cs — singleton + static Fade(float, Action) entrypoint preserved; phase machine FadingOut → Blackout → Invoke → FadingIn

Track B — Effects (sonnet)

  • GlitchBurst.cs — phase { Idle, Split, Flash, FadeIn, Done }; ghost-leak fix: _rGhost / _bGhost refs now persistent fields, destroyed at the top of every Burst() call and in OnDisable
  • ScanlineSweep.cstime-source fix: all accumulation now Time.unscaledDeltaTime; dead-alpha fix: replaced the broken revealProgress block with a single threshold test cg.alpha = (childCentreY >= scanlineY) ? 1f : 0f

Track C — FloatingFeedback (opus)

  • Single StepKind[] step-table per variant, one Update dispatch. 5 variants all preserve original timing 1:1:
  • Reward: Pop 0→1.2→1 / Float+fade 1.2s
  • AltarCompletion: Pop 0→1.4→1 / Float+fade 1.5s
  • Proximity: ColourFlash → Pop → FloatWithPulse (2s total, pulse mid-float)
  • Death: Glitch → Wait → HoldScale 1.1 → Settle → Fade 2s
  • Evolution: Glitch × 2 → PopScale → Hold 2s → Glitch → Fade
  • Destroy(gameObject) at final step with a _done guard (no double-invoke)

Track D — Clinical Components (opus)

  • ClinicalButton.cs — 5 coroutines replaced with per-channel structs (ColorTween, CornerTween) + PressPhase enum (3-phase interruptible punch) + sin-wave focus pulse tick. All 6 EventSystem interface methods preserved; SetDisabled preserved
  • ClinicalBar.csPhase { Idle, FillTween, CompleteBurst, Dormant } main states; DangerPulse runs orthogonally driven by _isInDanger; gain uses 2-phase overshoot (0.08s EaseOutCubic + 0.15s EaseOutBack), loss snaps, threshold-crossing snaps (no tween per plan)
  • OnDisable on both components resets transform scale, colours, CanvasGroup alpha, anchoredPosition

Track E — EndScreenController (sonnet)

  • DataLoadReveal coroutine replaced with RevealPhase { Idle, WaitingResult, WaitingTime, Done }. Same 0.05s per-row delay preserved. OnRematchClicked / OnLobbyClicked / ShowEndScreen signatures unchanged.

Gate G1 — UITween Delete

After Track D confirmed zero remaining using UITween in ClinicalButton, Assets/ProtoV2/Scripts/UI/Animation/UITween.cs and its .meta were deleted. Grep across Scripts/UI/ confirmed no other consumers.

Validation

| Check | Result |

|-------|--------|

| Compile clean (refresh_unity + read_console) | 0 errors |

| Coroutine grep in Scripts/UI/Animation | 0 matches |

| Coroutine grep in Scripts/UI/Components | 0 matches |

| Coroutine grep in Scripts/UI/Effects | 0 matches |

| UITween.cs + .meta deleted | confirmed |

| MainMenu screenshot: 3 buttons, title, subtitle, symmetric L-brackets | confirmed (see attached capture) |

| Stagger fade-in plays correctly (no permanently-hidden buttons) | confirmed — the original bug that drove this refactor is fixed |

Out-of-Scope Coroutines (Expected)

The grep flagged 4 files with remaining coroutines, all intentionally excluded per the plan:

  • UI/TerminalPhaseBloomController.cs — Phase 4 bloom blending; not UI animation
  • UI/CRTOverlayController.cs — Phase 4 shader post-process; its GlitchBurst() method is a coincidental name clash
  • UI/DevPanel.cs — dev tool, not runtime UI
  • UI/FloatingRewardIndicator.cs — legacy world-space indicator superseded by FloatingFeedback; removal is a separate cleanup task

Intentional Deviations from Plan

Track D reported two minor deviations, both intentional:

1. DangerPulse frequencies — plan specified 2 Hz / 8 Hz / 0.6 min-alpha; old code used 1.5 Hz / 3 Hz / 0.55. Executor chose to match the plan's explicit numbers. Net visual change is subtle; adjust in the inspector if preferred.

2. CompleteBurst scale — plan said "scale up 10%"; old code used 1.2× (20%). Executor kept the original 20% to preserve "visual fidelity 1:1" per the shared invariant. One-line change to switch if the plan's 10% was intended literally.

Flagging these here rather than changing code — user should decide which numbers stay.

Bugs Fixed in Passing

1. GlitchBurst ghost-leak on rapid re-Burst — previous coroutine version's StopCoroutine path leaked child ghost GameObjects; the coroutine-natural-end cleanup never ran. Fixed by storing ghost refs as fields and destroying them at the top of every Burst() call.

2. ScanlineSweep Time.deltaTime bug — previous version paused during timeScale = 0 which was inconsistent with every other UI animation; now uses Time.unscaledDeltaTime.

3. ScanlineSweep dead alpha math — broken revealProgress computation replaced with a clean threshold test.

Files Changed

| File | Action |

|------|--------|

| Scripts/UI/Animation/ButtonGroupStagger.cs | rewritten |

| Scripts/UI/Animation/PanelEntrance.cs | rewritten |

| Scripts/UI/Animation/SceneTransition.cs | rewritten |

| Scripts/UI/Animation/FloatingFeedback.cs | rewritten |

| Scripts/UI/Animation/UITween.cs | deleted |

| Scripts/UI/Components/ClinicalButton.cs | rewritten |

| Scripts/UI/Components/ClinicalBar.cs | rewritten |

| Scripts/UI/Effects/GlitchBurst.cs | rewritten + bug fix |

| Scripts/UI/Effects/ScanlineSweep.cs | rewritten + 2 bug fixes |

| Scripts/UI/EndScreenController.cs | rewritten |

Files Untouched (Verified)

Easing.cs, CornerMarkBreathe.cs, FloatingFeedbackSpawner.cs, MatchTimerUI.cs, UnifiedBar.cs, DeathHandler.cs, MainMenuController.cs, PauseController.cs, UIBuilder.cs, CRTOverlayController.cs, TerminalPhaseBloomController.cs.

All external callsites (SpawnReward, SpawnEvolution, SpawnDeath, Burst, Reveal, SetDisabled, Fade, OnRematchClicked, OnLobbyClicked) still resolve against locked public API signatures.

Follow-ups

  • Wire SceneTransition.Fade(...) into scene loads (no current callsites — the API was written against a design doc but never plumbed)
  • Wire FloatingFeedbackSpawner.SpawnAltarCompletion and SpawnProximity at the appropriate gameplay events (design doc specifies them but callsites don't exist yet)
  • Decide on DangerPulse frequency values (plan's 2/8Hz vs old 1.5/3Hz) and CompleteBurst scale (10% vs 20%)
  • Schedule a deletion pass for legacy FloatingRewardIndicator.cs once all callsites confirm they've moved to FloatingFeedback
  • Phase 6 (Match HUD overlay conversion) remains the next major session — now on a clean animation foundation

---

---

date: 2026-04-18

session: Menu Polish — Corner Marks, Hover Feedback, Pixel-Perfect Canvas Rule

status: complete

refs:

  • "Games/Ritual & Ruin/Confirmed/UI Visual Guidelines.md"
  • "Games/Ritual & Ruin/Options/UI Decisions Register.md"

---

Menu Polish — Corner Marks, Hover Feedback, Pixel-Perfect Canvas Rule

Goal

Make the MainMenu and Pause look presentable. Specifically:

1. Corner marks should render with uniform stroke thickness across all four pivots (TL / TR / BL / BR). Prior behavior: TR/BL/BR strokes appeared thinner than TL.

2. Hover feedback on buttons should be visible without bloom — both the corner marks AND the button background should change noticeably.

3. Strip redundant [ ] brackets from button labels now that corner marks provide the bracket visual.

4. Remove the main-menu subtitle "SURVIVAL IS MEASURED, NOT CELEBRATED" (user decision this session).

5. Document the pixel-perfect rule so it doesn't silently re-break the next time someone adds a Canvas.

The Root Cause of the Lopsided Corners

The CornerMark_* sprites are 32×32 source with 4-pixel strokes; they display in a 16×16 RectTransform (2:1 downscale → 2 pixel strokes at 1:1 display). That works when the Canvas renders at 1:1 scale. It breaks when CanvasScaler produces any non-integer render scale.

Example: reference 1920×1080 displayed at 1280×720 → 0.667× scale → a 2-pixel stroke becomes a 1.33 screen-pixel stroke. Unity then subpixel-rounds per pivot:

  • (0, 1) (TL) → rounds up → 2 pixels
  • (1, 1) / (0, 0) / (1, 0) → round down → 1 pixel

Three of four corners rendered half as thick as TL. Point-filter couldn't help (no anti-aliasing across the fractional boundary). Bilinear filter smoothed it at the cost of visible blur.

Real fix: Canvas.pixelPerfect = true — forces integer-pixel rendering regardless of scaler, so Point filter renders identical stroke counts at every pivot.

Changes

Sprites — `Assets/ProtoV2/Scripts/Editor/CornerMarkSpriteBuilder.cs`

  • STROKE bumped 3 → 4 (even number divides cleanly at 2:1 downscale)
  • Diamond pip drawing commented out (user wanted clean Ls, no interior accent)
  • Added 4 hover variants: CornerMark_TL_Hover / TR / BL / BR with arms extended from 14 → 22 pixels (at 32×32 source)
  • Single-pass Color32[] buffer + SetPixels32 (eliminated prior SetPixel / SetPixels32 mixing)
  • Filter mode remains FilterMode.Point (pixel-perfect Canvas makes it reliable)

UIBuilder — `Assets/ProtoV2/Scripts/UI/UIBuilder.cs`

  • New static void EnsurePixelPerfectCanvas(Transform child) helper — walks up to the root Canvas and sets pixelPerfect = true if not already
  • Every public factory method (AddCornerMarks, Button, Panel, ProgressBar, Slider, InputField, TabBar, Tooltip) now calls it as its first statement. Auto-enforcement — any Canvas hosting any UIBuilder widget gets pixel-perfect without manual setup
  • New LoadCornerMarkHoverVariants() method loads the 4 _Hover variants from Resources/UI/
  • Button() passes both idle and hover sprite arrays to ClinicalButton via new fields
  • Header comment documents the pixel-perfect rule inline

Button behaviour — `Assets/ProtoV2/Scripts/UI/Components/ClinicalButton.cs`

  • BgHover bumped #003B00 → #005500 — clearly visible mid-green vs almost-black
  • Added [HideInInspector] public Sprite[] cornerMarkIdleSprites and cornerMarkHoverSprites
  • New SetCornerSprites(bool hover) helper — instant Image.sprite swap on state transitions (no scale tween = no sub-pixel distortion)
  • Hover / Focus / Pressed states swap to _Hover sprites; Default / Disabled swap back to idle
  • Scale tween on corner marks removed (TweenCorners calls still there but pass 1f — the scale distortion was the original blur culprit in earlier iterations)
  • Focus pulse amplitude zeroed (gamepad focus communicated via color + longer arms only)

Menu content — `MainMenuController.cs` / `PauseController.cs` / `SettingsMenuBuilder.cs` / `InputRebindMenuUI.cs` / `InputRebindMenuSetup.cs`

  • Removed [ ] from button labels:
  • [ INITIATE EXPERIMENT ]INITIATE EXPERIMENT
  • [ CONFIGURE PARAMETERS ]CONFIGURE PARAMETERS
  • [ TERMINATE SESSION ]TERMINATE SESSION
  • [ RESUME EXPERIMENT ]RESUME EXPERIMENT
  • [ ABORT EXPERIMENT ]ABORT EXPERIMENT
  • [ CLOSE ]CLOSE
  • [ COMMENCE EXPERIMENT ]COMMENCE EXPERIMENT
  • Removed the SURVIVAL IS MEASURED, NOT CELEBRATED subtitle block from MainMenuController.BuildMainPanel
  • Manual _canvas.pixelPerfect = true removed from MainMenuController.Awake — UIBuilder now auto-enforces it; kept a comment pointing to the auto-enforcement path

Capture helper — `Assets/ProtoV2/Scripts/Editor/GameViewCapture.cs`

  • Bumped capture resolution 1280×720 → 1920×1080 so subpixel-rounding artifacts don't hide behind my own screenshot downscale

Documentation

  • CLAUDE.md rule #9 (new) — short in-project reference:

> Every Canvas that hosts a UIBuilder widget, ClinicalButton, ClinicalBar, or corner mark must have Canvas.pixelPerfect = true. UIBuilder auto-enforces this via EnsurePixelPerfectCanvas(). If you bypass UIBuilder and add widgets directly, set the flag yourself. See docs/UI_PIXEL_PERFECT.md.

  • docs/UI_PIXEL_PERFECT.md (new) — full technical explanation of the rule, the subpixel-rounding cause, what UIBuilder does to enforce it, when the rule breaks (manual Canvas creation), and what it does NOT affect (TMP text, world-space UI, shader filter modes)
  • UIBuilder class-header docblock has a dedicated PIXEL-PERFECT CANVAS RULE section

Validation

  • Compile clean (refresh_unity + read_console → 0 errors)
  • MainMenu screenshot at 1920×1080 capture: all 4 corners on each button render with uniform 2-pixel stroke thickness, no blur
  • Forced-hover screenshot (temporary debug; reverted): corner arms visibly extend from 14px to 22px (_Hover sprite swap), bg shifts to #005500, labels shift to #39FF14
  • Grep \[ .+ \] in Assets/ProtoV2/Scripts/ returns zero button-label matches (remaining hits are doc-comment examples in UIBuilder.cs which still use the old [ START ] style string — cosmetic, can be updated later)

Open Follow-ups

  • Hover interaction has not yet been tested with a real mouse cursor (MCP can't simulate pointer events cleanly). User to confirm in play mode that hover triggers the sprite swap + bg color change.
  • UIBuilder.cs header comment still references [ START ] style labels in one example line — update to bracket-less style on next docs pass.
  • The hover sprite asset generator uses a constant STROKE = 4; if you later want thicker hover strokes for even more pop, bump STROKE or add a separate STROKE_HOVER constant.

Why This Matters

These are small visual-polish fixes, but the pixel-perfect rule is the kind of bug that silently re-breaks UI whenever someone spawns a new Canvas and forgets the invariant. Enforcing it at the UIBuilder level + documenting it in three places (CLAUDE.md, docs/UI_PIXEL_PERFECT.md, UIBuilder header) gives us three chances for the rule to be seen before it's broken again.

In the latest dev session, we made some cool tweaks to Ritual & Ruin that should brighten up your experience! The chunk floor shader got a much-needed fix for those annoying white streaks that used to appear when moving around. We shifted our shadow calculations to work on a per-pixel basis, meaning smoother and more seamless shadows across the game world—no more glaring stripes where they shouldn't be!

We've also pushed Ritual & Ruin further into its dark kawaii aesthetic, which you might recognize as having that distinct anime vibe. The floor tiles now have a more dramatic contrast with near-white tops accented by violet hints, and darker sides bordered in dim purple grout. It's all about striking a balance between the cute and the mysterious.

On top of these changes, we've polished up the jellyfish players. They're sporting longer tentacles that extend their reach within matches, making every move more pronounced. Plus, they've become more visible against darker floors with increased opacity—so no more squinting to spot your jellyfish on the battlefield! All these tweaks aim to make the visuals pop while ensuring everything looks sharp and cohesive. Can't wait for you all to see how it turns out in action!

Raw session notes

2026-04-07 — Chunk Floor Shader, Shadow Fix, Visual Polish

Summary

Fixed a persistent white-streak artifact on the chunk floor shader, iterated on the dark kawaii visual direction, and polished jellyfish player visuals.

Dark Kawaii Visual Direction

Iterated the ChunkFloorTile shader toward a dark kawaii / anime aesthetic:

  • Top face: near-white tiles with violet hint (0.93, 0.90, 1.00) + neon lavender grout (0.72, 0.38, 0.95) with glow emission
  • Side faces: near-black tiles (0.05, 0.05, 0.09) + dim purple grout (0.48, 0.36, 0.70)
  • Face detection: branchless yDom = step(absNorm.x, absNorm.y) * step(absNorm.z, absNorm.y) drives both color palettes
  • Degradation: Voronoi crack lines with pink/magenta glow (0.95, 0.18, 0.62), FBM staining, mold growth
  • Crack scale: lowered _CrackScale to 0.5 (few large cracks rather than fine network)
  • Tile scale: doubled _TileScale to 1.0 (larger tiles)
  • Cel shading: floor(NdotL * bands) / (bands - 1) quantized diffuse, 3 bands

---

Visual Polish

Jellyfish Tentacles — Longer

  • segmentLength: 0.150.35 (scale=1 normalized, auto-scaled at runtime)
  • At match scale=0.25: total tentacle reach ≈ 0.525 world units (was 0.225)

Jellyfish Body — More Opaque

  • _Opacity in JellyfishBody.mat: 0.180.65
  • Players are now clearly visible against the dark floor

---

Files Changed

  • Assets/ProtoV2/Shaders/ChunkFloorTile.shader — per-fragment shadow coord, dark kawaii style
  • Assets/ProtoV2/Materials/ChunkFloorTile.mat — color values for dark kawaii look
  • Assets/ProtoV2/Scripts/JellyfishVisuals.cs — segmentLength 0.15→0.35
  • Assets/ProtoV2/Materials/JellyfishBody.mat — _Opacity 0.18→0.65

### Summary of Recent Changes and Developments

#### Jellyfish Tentacle Reversion (2026-04-03)

**Issue:** - The implementation of ObiRope tentacles for jellyfish caused excessive bouncing during bell pulses, as they reacted to the physics of the visual root squishing.

**Solution:** - Reverted to using a LineRenderer spring-chain setup. - Adjusted segment lengths and scaling to ensure visibility across different character scales (e.g., match scale 0.25, lobby scale 0.69).

**Implementation Details:** - Each tentacle consists of six segments that maintain proper spacing and length. - Size values are dynamically adjusted using `transform.lossyScale.x` for consistent appearance at varying scales.

#### Tilt Control Enhancements (2026-04-04)

**Objective:** - Complete integration of tilt control in various UI elements and settings, addressing gaps from a previous session.

**Key Changes:**

1. **Tilt in Rebind UIs:** - Added "Tilt" to the rebindable actions in `InputRebindMenuUI` and `PlayerSettingsPanel`. - Updated `InputRebindUIController` for controller presets, ensuring correct bindings across different control schemes.

2. **Auto-Tilt Toggle Wiring:** - Confirmed existing wiring via `PlayerMenuSetupWizard`, ensuring functionality is intact. - The toggle is accessible only to controller players and adjusts based on their input mode (visible in the settings panel accessed through menu).

3. **Per-Player Menu Hint Text:** - Introduced a new UI prompt system to guide players on accessing settings during a match. - Updated `PlayerMenuController` to dynamically display hint text based on game state and player device. - Enhanced `PlayerSettingsPanel` with instruction text for closing settings.

**Editor Wiring:** - Utilized `MenuHintWiringWizard` to automate the setup of menu hint texts in both the PlayerPrefab and LobbyScene, ensuring consistent UI elements across scenes.

### Architectural Insights

- **Tentacle Implementation:** Ensures visual consistency by auto-scaling and maintaining visibility across different game modes. - **Tilt Control Integration:** Provides a seamless experience for players using controllers, with clear guidance through dynamic hint texts. - **UI Consistency:** Automated wiring scripts ensure that UI elements are correctly set up and maintained, reducing manual errors.

These updates collectively enhance the player experience by refining visual effects and improving control accessibility, ensuring a smoother gameplay interaction across various modes and devices.

Raw session notes

2026-03-29 — SFX Options for All Assigned Slots

Summary

Added alternative option files for every currently-assigned SFX slot so the user can audition and swap without losing the existing working sounds.

AUDIO.md

Added ## SFX Options — Assigned Slots (Alternatives) section to playinstigator_docs/Games/Ritual & Ruin/AUDIO.md with per-slot tables listing all downloaded files and Freesound URLs.

---

Next Steps

  • Open Unity, navigate to Assets/ProtoV2/Audio/SFX/Options/ in Project window
  • Audition each folder to find preferred alternatives
  • Copy chosen file to correct SFX/ subfolder, rename, re-assign in Inspector

---

2026-03-29 — Audio Variation System + SFX Options

Summary

Built centralized audio variation system and sourced replacement options for all unassigned SFX slots.

---

AudioVariation System

Created Assets/ProtoV2/Scripts/Audio/AudioVariation.cs — a single [Serializable] class embedded in every audio script as one Inspector foldout. Replaces the scattered per-script variation fields that were being duplicated.

Parameters: pitch ±, volume ±, startTime (random clip offset), low-pass cutoff ±, reverb ±

Methods:

  • Init(go) — called in Awake, caches AudioLowPassFilter / AudioReverbFilter if present
  • PlayOneShot(source, clip, vol) — variation + PlayOneShot
  • PlayLoop(source, clip, vol) — variation + pitch/filter + Play() with loop=true
  • PlayFromOffset(source, clip, vol) — variation + startTime + Play() non-looping (throttled impacts)

All 6 audio scripts updated to use it: AudioManager, PlayerAudioSource, AltarAudioSource, BloodEmitterAudioSource (two instances — loop + burst), FloorImpactAudio, UIAudioManager.

Bug fixed: FloorImpactAudio.SharedImpactClip was never being set at runtime — floor blood impacts were silent. Fixed by adding bloodImpactClip field to AudioManager and wiring the static in Awake(). Requires assigning blood_impact.ogg to the new "Floor Impact" field on the AudioManager GO in Inspector.

---

SFX Audit

Checked which audio slots are actually assigned vs. missing. Found that several "removed" clips still have files on disk but are unassigned in the Inspector:

| Slot | File | Status |

|---|---|---|

| deathClip | SFX/Character/player_death.ogg | Exists but unsuitable |

| formTransformClip | SFX/Character/form_transform.ogg | Exists but unsuitable |

| flowLoopClip | SFX/Fluid/emitter_flow.ogg | Exists but unsuitable |

| All 5 UIAudioManager clips | SFX/UI/*.ogg | Exist but unsuitable |

---

SFX Options Downloaded

Downloaded and organised replacement options to Assets/ProtoV2/Audio/SFX/Options/:

| Folder | Files | Sources |

|---|---|---|

| Options/Death/ | 9 | Kenney Sci-Fi Sounds (slime, explosionCrunch), Kenney Impact Sounds (impactSoft_heavy) |

| Options/Transform/ | 8 | Kenney Sci-Fi Sounds (forceField x5, computerNoise x2) |

| Options/FlowLoop/ | 9 | Kenney Sci-Fi Sounds (spaceEngineLow, engineCircular, spaceEngineSmall) |

| Options/UI_Click/ | 11 | Kenney Interface Sounds (click x5), Kenney UI Audio (click x5, mouseclick) |

| Options/UI_Hover/ | 12 | Kenney UI Audio (rollover x6), Kenney Interface Sounds (scroll x4, tick x2) |

| Options/UI_Pause/ | 15 | Kenney Interface Sounds (toggle x4, switch x7, open x2, close x2) |

| Options/UI_PlayerJoin/ | 7 | Kenney Interface Sounds (confirmation x4, bong, pluck x2) |

| Options/UI_Transition/ | 9 | Kenney Interface Sounds (maximize x5, glitch x4) |

All Kenney files are CC0. Total: 80 auditionable options across 8 slots.

Additional Freesound CC0 and Pixabay options (40+ more) documented in AUDIO.md with direct URLs for manual download (Freesound requires login).

---

AUDIO.md Updated

playinstigator_docs/Games/Ritual & Ruin/AUDIO.md updated with:

  • Corrected slot statuses (file-exists-but-unsuitable vs. truly unassigned)
  • Full Options section per slot — downloaded files + manual-download Freesound/Pixabay links
  • How-to-assign workflow
  • Credits table updated with Kenney Sci-Fi Sounds and Impact Sounds

---

Next Steps

  • Audition Options files in Unity, pick one per slot, rename and assign in Inspector
  • Assign blood_impact.ogg to AudioManager GO's "Floor Impact" field (fix for silent floor impacts)
  • For slots where no Kenney option feels right: download preferred Freesound/Pixabay option using the URLs in AUDIO.md

---

Evolution Size Fix + Asset Catalog + Resolved Issue Cleanup (2026-03-29)

---

1. Terminal Evolution Size Fix

Issue

Terminal evolution (Tier 2) scale multiplier of 1.5× made the jellyfish too large to pass through floor holes.

Fix

File: Assets/ProtoV2/Scripts/JellyfishVisuals.cs — line 194

Scale progression (final):

| Tier | Scale | Visual cues |

|---|---|---|

| Base (0) | 1.0× | White tint, squish 0.20 |

| Enhanced (1) | 1.2× | Blue tint, squish 0.28 |

| Terminal (2) | 1.3× | Gold tint, squish 0.38, pulse 0.75× faster |

Terminal is still visually distinct (gold color, faster pulse, more squish, slightly larger) but now fits through floor holes.

---

2. Resolved Issues Cleanup

Removed the following items from the open blockers list — all confirmed resolved:

  • TQ-4 (Pause + ObiFluid): ObiFluid particles correctly pause/resume with Time.timeScale = 0. No code changes needed.
  • maxSurfaceChunks: Already reduced to 2000 in ObiSolver Inspector. No further action needed.
  • ObiNativeList finalizer: Mitigated by D3D11 (GPU buffers handled more gracefully). Lower priority, not blocking.

Open blockers: None.

---

3. Asset Catalog

Created Confirmed/strategy/Asset Catalog.md — full inventory of 38 purchased Unity assets with applicability ratings for Ritual & Ruin.

Summary:

  • Tier 1 (use these): Feel, Amplify Shader Editor, Amplify Shader Pack, Obi Rope, Obi Softbody, Motion Blur, Magic Effects FREE, DOTween
  • Tier 2 (potentially useful): Elemental Spells VFX, Easy Save, KayKit Platformer Pack, Weather Maker, Fantasy RPG GUI, Obi Cloth
  • Tier 3 (not relevant): Humanoid character packs, city environments, turret assets, tank, fruit market, 2D tools

Priority integration order: Feel → Obi Rope → Motion Blur → Magic Effects FREE → Easy Save → Amplify Shader Editor

---

4. Evolution Visual Distinction — Next Steps

Current visual distinction between tiers (post-fix):

  • Size: 1.0 → 1.2 → 1.3 (subtle)
  • Color: white → blue → gold ✅ clear
  • Squish: 0.20 → 0.28 → 0.38 (subtle)
  • Pulse rate: unchanged → unchanged → 0.75× faster ✅ readable

Suggested enhancements using owned assets:

  • Feel: Add camera shake burst + flash on evolution event (OnEvolution subscriber)
  • Magic Effects FREE: Spawn a VFX burst prefab at player position on evolution
  • Obi Rope: Upgrade tentacles to physical simulation (more dramatic swing on evolution)
  • Amplify Shader Editor: Add emissive glow increase per tier to jellyfish bell material

Design discussion needed on which to prioritize.

---

Jellyfish Squish + Emitter Indicator + Scroll Camera Fix (2026-03-29)

---

1. Jellyfish Cap Horizontal Squish

Issue

XZ bulge on pulse was 30% of squish amount — too subtle.

Fix

File: Assets/ProtoV2/Scripts/JellyfishVisuals.cs

XZ bulge doubled — bell now visibly widens on each pulse. Spring recovery unchanged.

---

2. Emitter Indicator — No Longer Lands on Players

Issue

BloodEmitterIndicator.DrawCircle() used Physics.Raycast with no layer filter. When a player flew between the emitter and the floor, the raycast hit the player's collider, placing the circle on top of the player.

Root Cause

All objects (players, floors, walls) are on the Default layer — no layer separation to filter against.

Fix

File: Assets/ProtoV2/Scripts/BloodSystem/BloodEmitterIndicator.cs

Replaced physics raycast entirely with chunk-coordinate lookup. We already know the grid layout — no need to cast rays:

Walks top→middle→bottom floor. Finds first floor that has a solid chunk at the emitter's XZ grid position. Surface Y derived from floor root + ChunkHeight/2. Removed raycastLength field entirely.

---

3. Camera Scroll — Single Source of Truth for Floor Spacing

Issue

ScrollingFloorCamera had its own [SerializeField] float floorVerticalSpacing = 5f independent from FloorManager.floorVerticalSpacing. If the two values differed in the Inspector, the camera would scroll to the wrong Y position.

Fix

File: Assets/ProtoV2/Scripts/CameraSystem/ScrollingFloorCamera.csCalculateFloorYPosition()

Camera's own floorVerticalSpacing field is now only a fallback for editor gizmos. At runtime, both floor generation and camera scroll use the same value from FloorManager.

Current values (defaults)

  • FloorManager.floorVerticalSpacing = 5f — adjust this to change floor spacing
  • ScrollingFloorCamera.scrollDuration = 2.5f — how long the scroll animation takes

---

4. Ritual Completion VFX — AltarVFXController

Status

Feel (More Mountains) and Magic Effects FREE (Hovl Studio) are now imported.

New File

Assets/ProtoV2/Scripts/BloodSystem/AltarVFXController.cs — attach alongside AltarParticleConsumer on the Altar prefab.

Drives:

  • OnMeterFull → spawn looping countdown VFX (e.g. Magic circle.prefab or Healing circle.prefab)
  • OnRitualComplete → destroy countdown VFX + spawn completion burst (e.g. Red energy explosion.prefab) + play MMF_Player feedbacks (screen shake, flash, audio)
  • OnAltarReset → destroy countdown VFX

Inspector Wiring Required (on Altar prefab)

1. Add AltarVFXController component

2. Create child GO with MMF_Player, add Screen Shake + Camera Flash + Audio feedbacks → assign to completionFeedbacks

3. Assign completionVFXPrefab = Red energy explosion.prefab (Hovl Studio Magic Effects)

4. Assign countdownVFXPrefab = Magic circle.prefab or Healing circle.prefab

5. Tune completionVFXYOffset and countdownVFXYOffset to sit correctly at altar ground level

Also Available

  • Elemental Spells Full Pack VFX — still in .unitypackage archive, not yet imported. Contains additional burst/explosion options for ritual completion.

---

5. Lobby Scene — Tentacle Scale Fix

Issue

JellyfishVisuals tentacle rim positions were set in world-space Inspector values. characterScale in LobbyScene was 1.0 while MatchScene uses 0.25. At scale 1, the bell is 4× larger than the rim radius (0.4), so tentacles appeared hidden inside the player body.

Fix — Two Parts

Part A: LobbyScene.unityMultiplayerManager.characterScale changed 10.25 so all scenes use the same scale.

Part B: JellyfishVisuals.csBuildTentacles() and UpdateTentacles() now multiply all spatial values by transform.lossyScale.x (ws):

Inspector values are now local-space (as if characterScale = 1). ws scales them to world-space at runtime, so tentacles are always proportionally correct regardless of characterScale.

Part C: PlayerPrefab.prefab Inspector values converted from old world-space to new local-space (÷ 0.25 = × 4):

| Field | Before (world-space) | After (local-space) |

|---|---|---|

| rimRadius | 0.4 | 1.6 |

| rimY | 0.56 | 2.24 |

| segmentLength | 0.15 | 0.6 |

| startWidth | 0.15 | 0.6 |

| endWidth | 0.04 | 0.16 |

At characterScale = 0.25, ws = 0.25, so effective world-space values are identical to before.

---

Dev Log — 2026-03-29 — MMFeedbacks Integration

Summary

Full MMFeedbacks (Feel) integration across all major gameplay events in Ritual & Ruin. Every significant player action, match event, and altar moment now triggers camera shake and/or freeze-frame feedback.

---

What Was Done

AltarVFXController — Namespace Fix

  • File: Assets/ProtoV2/Scripts/BloodSystem/AltarVFXController.cs
  • Added using CupFairy.BloodSystem; to fix CS0246 compile error (AltarParticleConsumer not found)
  • Controller was already written; this unblocked it from compiling

Script Changes — MMF_Player Fields Added

All scripts received using MoreMountains.Feedbacks; + a [SerializeField] private MMF_Player field + a ?.PlayFeedbacks(transform.position) call at the relevant event moment:

| Script | Field | Trigger |

|---|---|---|

| DeathHandler.cs | deathFeedback | Before OnDied?.Invoke() |

| UnifiedBar.cs | evolutionFeedback | Before OnEvolution?.Invoke() in TriggerEvolution() |

| CharacterController1.cs | pulseFeedback | After PulsedThisFrame = true in pulse timer |

| TransformController.cs | formToggleFeedback | At start of Toggle() |

| MatchCountdown.cs | countdownBeatFeedback, goFeedback | Each countdown beat tick; GO! display |

| MatchManager.cs | teamEliminatedFeedback, matchEndFeedback | RecordTeamEliminated(); EndMatch() |

PlayerPrefab — 4 Feedback Child GOs

Added 4 new child GameObjects under the PlayerPrefab root, each with a Transform + MMF_Player component:

| GO Name | fileID | Wired to Script Field | Feedbacks |

|---|---|---|---|

| DeathFeedbacks | &1111111111111111103 | DeathHandler.deathFeedback | FreezeFrame 0.08s + PositionShake 0.4s/range 0.5 |

| EvolutionFeedbacks | &1111111111111111106 | UnifiedBar.evolutionFeedback | FreezeFrame 0.05s + PositionShake 0.3s/range 0.3 |

| PulseFeedbacks | &1111111111111111109 | CharacterController1.pulseFeedback | PositionShake 0.1s/range 0.05 (subtle — fires every 0.35s) |

| FormToggleFeedbacks | &1111111111111111112 | TransformController.formToggleFeedback | PositionShake 0.2s/range 0.15 |

All 4 Transform fileIDs (1111111111111111102, 105, 108, 111) added to root Transform m_Children list (fileID 5020478598309948295).

MatchScene — 4 Feedback Child GOs + Scene Components

Previously added (pre-this-session):

  • MMPositionShaker on Main Camera
  • MMTimeManager GO in scene
  • 4 feedback child GOs under MatchCountdown and MatchManager

This session — populated the MMF_Player feedbacks in each GO:

| GO Name | fileID | Wired to Script Field | Feedbacks |

|---|---|---|---|

| CountdownBeatFeedbacks | &1482767222 | MatchCountdown.countdownBeatFeedback | PositionShake 0.1s/range 0.1 |

| GoFeedbacks | &872176054 | MatchCountdown.goFeedback | FreezeFrame 0.05s + PositionShake 0.3s/range 0.3 |

| TeamEliminatedFeedbacks | &247481994 | MatchManager.teamEliminatedFeedback | FreezeFrame 0.06s + PositionShake 0.35s/range 0.4 |

| MatchEndFeedbacks | &1200068797 | MatchManager.matchEndFeedback | FreezeFrame 0.1s + PositionShake 0.5s/range 0.5 |

Altar Prefab — Completion Feedbacks (Previous Session)

Already done: CompletionFeedbacks child MMF_Player has:

  • MMF_Light (existing)
  • MMF_FreezeFrame (0.05s)
  • MMF_PositionShake (0.3s/range 0.3, random XY)

---

Feedback Intensity Rationale

| Event | Intensity | Reasoning |

|---|---|---|

| Match end | Strongest (0.1s freeze, 0.5 shake) | Game-ending moment, maximum impact |

| Player death | Strong (0.08s freeze, 0.5 shake) | High-stakes moment |

| Team eliminated | Medium-strong (0.06s freeze, 0.4 shake) | Significant match event |

| GO! start | Medium (0.05s freeze, 0.3 shake) | Match start signal |

| Evolution | Medium (0.05s freeze, 0.3 shake) | Power-up moment |

| Altar completion | Medium (0.05s freeze, 0.3 shake) | Ritual payoff |

| Form toggle | Subtle (0.2s shake/0.15 range) | Frequent action, shouldn't fatigue |

| Countdown beat | Subtle (0.1s shake/0.1 range) | Rhythmic, very frequent |

| Staccato pulse | Minimal (0.1s shake/0.05 range) | Fires every 0.35s, must be imperceptible individually |

---

Technical Notes

  • All feedbacks use managed reference YAML format (FeedbacksList: + references: RefIds:) required by MMF_Player v3
  • Scene MMF_Players use a hybrid format (Feedbacks: [] legacy field + FeedbacksList: + references:) — both must be present
  • MMTimeManager GO required in scene for FreezeFrame to function (timescale manipulation)
  • MMPositionShaker on Camera required to receive PositionShake events (channel 0)
  • Owner field in each feedback data points to the MMF_Player component's own fileID
  • All PositionShake feedbacks: RandomizeDirectionX: 1, RandomizeDirectionY: 1, RandomizeDirectionZ: 0 (2D shake only, no depth)

---

Files Modified

  • Assets/ProtoV2/Scripts/BloodSystem/AltarVFXController.cs
  • Assets/ProtoV2/Scripts/DeathHandler.cs
  • Assets/ProtoV2/Scripts/UnifiedBar.cs
  • Assets/ProtoV2/Scripts/CharacterController1.cs
  • Assets/ProtoV2/Scripts/TransformController.cs
  • Assets/ProtoV2/Scripts/MatchCountdown.cs
  • Assets/ProtoV2/Scripts/MatchManager.cs
  • Assets/ProtoV2/Prefabs/PlayerPrefab.prefab (4 new GO+Transform+MMF_Player blocks)
  • Assets/ProtoV2/Scenes/MatchScene.unity (4 MMF_Player feedbacks populated)

---

Obi Fluid Memory Leak — Continued Investigation (2026-03-29)

Continued from 2026-03-28_ObiFluid_MemoryLeak_Fix.md. Previous session patched the rendering-side leak (VolumePass materials, MaterialPropertyBlock, AsyncGPUReadback closures). This session found and addressed the simulation-side leak.

---

Monitoring Setup

PowerShell script at C:/Users/ReconUnPro/AppData/Local/Temp/monitor_mem2.ps1 — polls every 5s:

  • Game process working set (MB) via Get-Process
  • System RAM available / used / % via Get-CimInstance Win32_OperatingSystem

---

Isolation Test Results

Build #006 — All Obi Rendering Disabled

Disabled three layers to fully strip rendering:

1. Removed ObiFluidRendererFeature from m_RendererFeatures in PC_Renderer.asset

2. ObiParticleRendererm_Enabled: 0 on BloodEmitter.prefab

3. ObiFluidSurfaceMesherm_Enabled: 0 on BloodEmitter.prefab

Result: Memory still grew at ~161 MB/s. Flush/scene reload gave no relief.

Conclusion: Leak exists on BOTH sides:

  • Rendering side: ~320 MB/s (patched in prior session, confirmed by rate drop)
  • Simulation side: ~161 MB/s (still present — this session's target)

---

Patches Applied This Session

Patch 3 — `ObiSolver.PushActiveParticles()` (previously applied)

Pre-allocate activeParticles to allocParticleCount before clear so EnsureCapacity never fires mid-emission.

Patch 4 — `ComputeSolverImpl.SetActiveParticles()` (previously applied)

Guard: only call AsComputeBuffer if buffer is null. Otherwise UploadFullCapacity().

Patch 5 — `ObiSolver.PushSimplices()`

File: Assets/Obi/Scripts/Common/Solver/ObiSolver.cs

Added before simplices.Clear():

Why: dirtySimplices is set every time a particle is activated (every emission frame). Without pre-allocation, EnsureCapacity fires repeatedly as the active count grows, each time doubling capacity and nulling m_ComputeBuffer.

Patch 6 — `ComputeSolverImpl.SetSimplices()`

File: Assets/Obi/Scripts/Common/Backends/Compute/Solver/ComputeSolverImpl.cs

Replaced unconditional AsComputeBuffer calls on simplices (line 515) and cellCoords (line 516):

Result of builds #006-#007: Memory still grew at ~161 MB/s. These patches were correct but insufficient — the ROOT CAUSE was elsewhere.

---

Root Cause Found — Bloated Blueprint Capacity

The Discovery

Deep Obi source analysis by Opus architect agent identified the actual culprit.

BloodFluid.asset had capacity: 20000 per emitter.

With 9 emitters active (3 emitters/floor × 3 floors), the solver pre-allocates:

  • Total particle slots: 9 × 20,000 = 180,000
  • positions.count = 180,000 (used by SetCapacity)

This caused ComputeParticleGrid.SetCapacity() to create:

| Buffer | Size |

|---|---|

| neighbors (180k × 2 × 128 neighbors × 4B) | 184 MB |

| All 20+ grid buffers total | ~230 MB |

| colliderContacts readback buffer | ~46 MB |

| 26 particle arrays in ParticleCountChanged | ~170 MB |

~650 MB allocated at match start — before a single particle is emitted.

The D3D12 driver pools these on disposal (never returns to OS), so repeated SetCapacity calls accumulate permanently. colliderContacts.Readback() in ObiSolver.RequestReadback() fires every frame (whenever OnCollision != null, which our scripts always satisfy), staging a ~46 MB readback buffer per frame into D3D12's READBACK heap.

Why 20,000 Was Wrong

Our ObiEmitter runs in burst mode:

  • 7 particles/sec × 1.5s burst every 5s = ~2.1 particles/sec average
  • Particle lifespan: 60s
  • Peak live particles per emitter: 2.1 × 60 = ~126

20,000 capacity = 158× overprovision. The default Obi blueprint value was never adjusted for our actual usage.

The Fix

Assets/ProtoV2/Blueprints/BloodFluid.asset line 1559:

200 gives 1.6× headroom over peak (126 live particles). With 9 emitters:

  • Total particle slots: 9 × 200 = 1,800 (vs 180,000)
  • neighbors buffer: 1.8 MB (vs 184 MB) — 100× reduction
  • colliderContacts readback: ~460 KB (vs 46 MB) — 100× reduction

---

Secondary Root Cause — ObiNativeList Finalizer (Scene Reload Doesn't Help)

ObiNativeList.~ObiNativeList() calls DisposeOfComputeBuffer() from the GC finalizer thread. GraphicsBuffer.Dispose() must run on the main thread — called from the wrong thread, it silently fails. GPU buffers are permanently orphaned and survive scene reload.

This explains why Flush/scene restart never released memory — not a leak per se but a one-time orphan per solver lifetime.

Status: Not patched yet. Lower priority now that capacity is fixed.

---

Build Log

| Build | Change | Sim Leak Rate | Notes |

|---|---|---|---|

| #004 (prev) | Rendering patches | ~481 MB/s | Rendering still enabled |

| #005 | m_Active: 0 attempt | ~481 MB/s | Wrong YAML field — fluid still rendered |

| #006 | All rendering disabled | ~161 MB/s | Confirmed sim-side leak |

| #007 | SetSimplices patch | ~161 MB/s | Patch correct but not root cause |

| #008 | capacity: 200 + SetSimplices | ~134 MB/s | Baseline polluted (monitoring started mid-match at 20 GB); capacity patches correct but D3D12 root cause not yet identified |

| #009 | ObiSolver disabled | ~135 MB/s (polluted) / flat after reboot | Confirmed leak stops when match ends even without Obi; polluted D3D12 baseline made rate unreliable |

| #010 | ObiSolver disabled + D3D11 | ~0 MB/s | Flat 1280 MB throughout full match — confirmed D3D12 deferred-release pool is root cause |

| #011 | Full Obi re-enabled + D3D11 | ~0.1 MB/s | FIXED — 1337→1347 MB over 90s. Stable. |

---

Root Cause (Final)

The leak was D3D12-specific deferred-release pool behaviour, not a true memory leak in code.

  • D3D12: GraphicsBuffer.Dispose() queues the buffer for deferred release. The driver returns memory to an internal pool, NOT to the OS. New allocations commit fresh OS pages instead of reusing pooled ones. Pool grows monotonically for the lifetime of the process.
  • D3D11: Disposed resources are returned to the driver pool immediately and reused for new allocations. Working set stays flat.

Obi's emit/die particle lifecycle creates a small but steady stream of new GraphicsBuffer allocations per frame (SetSimplices, readbacks). On D3D11 these are reused. On D3D12 they permanently expand the pool.

Fix applied: ProjectSettings.asset — Standalone Windows graphics API forced to Direct3D11 (m_APIs: 02000000, m_Automatic: 0).

---

What Didn't Work / Lessons

  • m_Enabled: 0 on a URP ScriptableRendererFeature — WRONG field. URP checks m_Active, not m_Enabled.
  • Removing ObiFluidRendererFeature from m_RendererFeatures — correct way to disable URP features in YAML.
  • SetSimplices/SetActiveParticles guards — correct patches but not the root cause. The leak was D3D12 pool behaviour, not allocation frequency or size.
  • Capacity 20000→200 — correct and reduces one-time static allocation by 100×, but doesn't fix D3D12 pool growth.
  • Polluted D3D12 baseline (no reboot between test builds) made rate measurements unreliable across builds. Always reboot when comparing rates.
  • Source-only analysis hit limits — binary isolation (disable ObiSolver, then switch API) was faster than reading code.

---

Remaining Items

  • [x] Re-enable rendering (ObiFluidRendererFeature, ObiParticleRenderer, ObiFluidSurfaceMesher)
  • [x] Verify memory is stable with rendering re-enabled — CONFIRMED 0.1 MB/s
  • [x] Force D3D11 for all Standalone builds — ProjectSettings.asset permanently set (m_APIs: 02000000, m_Automatic: 0)
  • [ ] Fix ObiNativeList finalizer thread issue (GPU buffer orphaning on scene reload) — lower priority, one-time per session; mitigated by D3D11
  • [ ] Update maxSurfaceChunks if needed now that particle count is much smaller

---

Status: RESOLVED (2026-03-29)

Memory leak investigation is complete. Game is stable at ~0.1 MB/s (effectively flat). All Standalone builds will use Direct3D11 permanently for as long as Obi Fluid is in the project. Proceeding to next development phase.

---

2026-03-31 — Centralised Audio Management Panel

Summary

Added a GameAudioData ScriptableObject as a single source of truth for all 22 audio clip slots across the game. Includes a rich custom Inspector panel with category foldouts, clip-count badge, preview buttons, and auto-link tools.

---

Problem

Audio clips were scattered across 5 different components on 4 different prefabs/GOs:

  • AudioManager (MatchScene GO) — 9 clips
  • PlayerAudioSource (PlayerPrefab) — 5 clips
  • AltarAudioSource (Altar prefab) — 3 clips
  • BloodEmitterAudioSource (BloodEmitter prefab) — 2 clips
  • UIAudioManager (DontDestroyOnLoad GO) — 5 clips

To reassign a clip you had to hunt through 4 prefabs. No way to see the full audio inventory at a glance.

---

Solution

New Files

Assets/ProtoV2/Scripts/Audio/GameAudioData.csScriptableObject holding all 22 AudioClip fields + sfxGroup / uiGroup AudioMixerGroup refs, grouped by category:

  • Match/Countdown, Floor Scroll, Match Outcome, Outlast Ticker, Floor Impact
  • Player, Altar, Blood Emitter, UI

Assets/ProtoV2/Scripts/Editor/GameAudioDataEditor.cs — Custom Inspector with:

  • Bold header "Ritual & Ruin — Audio Data"
  • HelpBox badge: "X / 22 clips assigned" (warning if incomplete)
  • Per-category colour-coded foldouts (persisted via EditorPrefs) each showing assigned/total
  • Every slot: object field + preview button (AudioUtil reflection, fallback safe)
  • "Find & Link Clips from SFX Folder" button — scans Assets/ProtoV2/Audio/SFX/ and auto-assigns by filename pattern matching
  • "Find & Link Mixer Groups" button — loads GameAudioMixer from Resources and assigns SFX + UI groups

Assets/ProtoV2/Scripts/Editor/GameAudioDataWizard.cs — Menu items under Tools/Ritual & Ruin/Audio/:

  • Create GameAudioData Asset — creates Assets/ProtoV2/Audio/GameAudioData.asset, selects + pings it
  • Auto-Link Clips & Mixers — one-shot auto-fills the asset from the SFX folder + mixer, logs all assignments

Modified Files (backward-compatible)

Each of the 5 audio components gained:

1. [Header("Audio Data")] [SerializeField] private GameAudioData _audioData; as the first serialized field

2. An if-block at the top of Awake() that copies clips from _audioData into the existing private fields when assigned

Components affected:

  • AudioManager.cs — 9 clips + sfxGroup
  • PlayerAudioSource.cs — 5 clips + sfxGroup
  • AltarAudioSource.cs — 3 clips + sfxGroup
  • BloodEmitterAudioSource.cs — 2 clips + sfxGroup
  • UIAudioManager.cs — 5 clips + uiGroup

Backward compatible: if _audioData is null, all individual Inspector-assigned clips continue to work unchanged.

---

How to Set Up

1. Tools/Ritual & Ruin/Audio/Create GameAudioData Asset — creates the asset

2. Select it in Project → Inspector shows the full panel

3. Click "Find & Link Clips from SFX Folder" + "Find & Link Mixer Groups" to auto-fill

4. Assign the asset to the _audioData slot on: AudioManager (MatchScene), UIAudioManager (MainMenu scene), PlayerPrefab, Altar prefab, BloodEmitter prefab

Or use Tools/Ritual & Ruin/Audio/Auto-Link Clips & Mixers to do steps 2+3 from a menu item.

---

Files Changed

| File | Change |

|---|---|

| Assets/ProtoV2/Scripts/Audio/GameAudioData.cs | NEW — ScriptableObject, 22 clips + 2 mixer groups |

| Assets/ProtoV2/Scripts/Editor/GameAudioDataEditor.cs | NEW — Custom Inspector panel |

| Assets/ProtoV2/Scripts/Editor/GameAudioDataWizard.cs | NEW — Create + auto-link menu items |

| Assets/ProtoV2/Scripts/Audio/AudioManager.cs | +_audioData field + Awake copy block |

| Assets/ProtoV2/Scripts/Audio/PlayerAudioSource.cs | +_audioData field + Awake copy block |

| Assets/ProtoV2/Scripts/Audio/AltarAudioSource.cs | +_audioData field + Awake copy block |

| Assets/ProtoV2/Scripts/Audio/BloodEmitterAudioSource.cs | +_audioData field + Awake copy block |

| Assets/ProtoV2/Scripts/Audio/UIAudioManager.cs | +_audioData field + Awake copy block |

---

2026-03-31 — Obi Rope + Obi Softbody Jellyfish Upgrade

What Was Done

Replaced the procedural LineRenderer spring-chain tentacles with proper Obi Rope physics tentacles, and added an Obi Softbody jelly core system. Both packages (Obi Rope 7.x, Obi Softbody 7.x) were already imported by the developer.

---

Files Changed

`Assets/ProtoV2/Scripts/JellyfishVisuals.cs` — Full rewrite (tentacle system only)

Removed:

  • All LineRenderer fields: _tentacles, _tentacleMaterials, _segmentPositions, _rimOffsetsLocal
  • Inspector params: segmentCount, segmentLength, dampFactor, startWidth, endWidth, swayAmplitude, swayFrequency
  • Methods: BuildTentacles(), UpdateTentacles(), BuildTentacleMaterial()

Added:

  • [SerializeField] ObiSolver _solver — auto-found via FindObjectOfType if null; tentacles skipped gracefully if solver unavailable (bell squish + hover bob still work)
  • Inspector params: ropeLength=0.9, ropeVisualThickness=0.05, ropeBendCompliance=0.01, ropeParticleCount=8
  • Runtime arrays: _ropes, _blueprints, _anchorTransforms, _ropeMaterials
  • BuildRimOffsets() — same XZ rim circle math as before
  • BuildObiRopeTentacles() — creates 6 ObiRope actors at Start(), each parented under ObiSolver
  • UpdateAnchorTransforms() — updates anchor Transform world positions each frame (same formula as old anchor calc: world Y from VisualRoot + rim XZ from root direction)
  • CreateRopeMaterial() — URP/Unlit transparent material, team-colored

Unchanged: UpdateHoverBob(), UpdateBellSquish(), ApplyEvolutionTint(), SetTentacleColor(), all evolution/color fields, OnDestroy() cleanup pattern.

Rope setup per tentacle:

1. Anchor Transform child of root GO (NOT VisualRoot — avoids inheriting bell tilt)

2. ObiRopeBlueprint: 2 control points (root at 0,0,0 → tip at 0,-length,0), tapered thickness (tip = 40% of root), root color opaque → tip alpha=0

3. ObiRope + ObiPathSmoother + ObiRopeExtrudedRenderer (DefaultRopeSection)

4. ObiParticleAttachment.Static on groups[0] → anchor transform

5. Parent under solver LAST (triggers AddToSolver)

---

`Assets/ProtoV2/Scripts/JellyfishSoftCore.cs` — New

ObiSoftbody "jelly interior" component. Add to PlayerPrefab root. Requires a pre-baked blueprint (run wizard below).

At Start(): creates a sphere-shaped ObiSoftbody GO under the solver, dynamically pin-attached to the character root via ObiParticleAttachment.Dynamic with configurable _compliance (default 0.002 — elastic spring lag). Rendered with a translucent blue sphere.

Exposes:

  • PulseSquish(float inwardSpeed) — applies inward radial velocity to all particles on pulse
  • SetColor(Color) — evolution tinting
  • IsReady — safe call guard

---

`Assets/ProtoV2/Scripts/Editor/JellyfishSoftCoreSetupWizard.cs` — New

Menu: Ritual & Ruin / Setup Jellyfish Soft Core

One-click editor wizard that:

1. Gets Unity's built-in sphere mesh via GameObject.CreatePrimitive(PrimitiveType.Sphere)

2. Creates ObiSoftbodyBlueprint, assigns mesh, calls GenerateImmediate()

3. Saves blueprint to Assets/ProtoV2/SoftbodyBlueprints/JellyfishCoreSoftbody.asset

4. Opens PlayerPrefab via EditPrefabContentsScope, adds JellyfishSoftCore to root, wires blueprint

---

How to Use After Compilation

Obi Rope Tentacles (automatic)

  • Assign ObiSolver reference on JellyfishVisuals in PlayerPrefab Inspector, OR leave null (auto-found at runtime)
  • Enter Play mode → 6 ObiRope tentacles simulate under the solver, physically trailing behind character movement

Obi Softbody Jelly Core (opt-in)

1. Wait for scripts to compile

2. Run Ritual & Ruin / Setup Jellyfish Soft Core from the Unity menu

3. Enter Play mode → translucent sphere lags behind character with elastic follow

Tuning parameters (JellyfishSoftCore Inspector):

  • _compliance: 0.001 (tight) → 0.01 (very sloshy)
  • _coreOffset: default (0, 1.5, 0) — positions core inside bell
  • _coreScale: default 0.35 — sphere radius
  • _coreColor: default translucent blue-white (0.5, 0.85, 1, 0.25)

Tuning parameters (JellyfishVisuals Inspector):

  • ropeLength: 0.9 — tentacle length
  • ropeVisualThickness: 0.05 — cross-section radius
  • ropeBendCompliance: 0.01 — stiffness (0=rigid, 0.1=very floppy)
  • ropeParticleCount: 8 — simulation resolution

---

Architecture Notes

  • Rope GOs are parented under ObiSolver, NOT under the character. Anchor Transforms (children of character root) follow the character each frame; ObiParticleAttachment.Static pins rope particle 0 to each anchor.
  • Anchor Transforms are children of the root (not VisualRoot) so they don't inherit pour tilt or bell squish rotation.
  • OnDestroy() cleans up all rope GOs, blueprint ScriptableObjects (DestroyImmediate), materials, and anchor GOs — no leaks on respawn/rematch.
  • If ObiSolver not found at Start, bell squish and hover bob still work — only tentacles are skipped.
  • JellyfishSoftCore is fully optional. No changes to JellyfishVisuals required for softbody to work.

---

Design Vault Reference

  • Asset Catalog Tier 1: Obi Rope ✅ implemented, Obi Softbody ✅ implemented
  • Jellyfish Visuals confirmed doc: "Future: replace with ObiRopes" ✅ done

---

2026-04-03 — Tentacle Revert: LineRenderer Spring-Chain

What Changed

Reverted jellyfish tentacles from ObiRope back to LineRenderer spring-chain.

Root cause for revert: ObiRope tentacles are physical actors — on every bell pulse the VisualRoot squishes, which moves the ObiParticleAttachment anchor, causing particles to bounce violently. The tentacles should trail softly, not react to the bell pulse physics.

Implementation

JellyfishVisuals.cs — full rewrite

  • 6 LineRenderer tentacles, 6 segments each
  • Spring chain: segment 0 snaps to anchor, segments 1+ lerp toward (prev + Vector3.down * segLen) then distance-constrained to fixed length
  • Auto-scaling: all size values multiplied by transform.lossyScale.x at Start() so tentacles look correct at any character scale (match=0.25, lobby=0.69)
  • Inspector values are scale=1 normalized

Prefab values (scale=1 normalized)

| Field | Value | World value @ scale=0.25 |

|---|---|---|

| rimRadius | 1.3 | 0.325 |

| rimY | 1.5 | 0.375 (anchor above floor) |

| segmentLength | 0.25 | 0.0625/segment, 0.375 total = reaches floor |

| startWidth | 0.6 | 0.15 |

| endWidth | 0.16 | 0.04 |

Key insight on segmentLength: Originally set to 0.6 (= 0.9 world units total at match scale) — tentacles extended 0.525 units below floor, making only ~2 segments visible. Corrected to 0.25 (= 0.375 world units total) so all 6 segments are visible above floor. At lobby scale 0.69: 0.25 × 0.69 × 6 = 1.035 world units = exactly matches anchor height. Both scenes auto-balance.

TentacleSetupWizard.cs

Added "Ritual & Ruin/Cleanup Jellyfish Tentacles (Revert to LineRenderer)" menu item that removes TentacleSolver GO and TentacleAnchor_* from VisualRoot. ObiRope setup code kept shelved for future reference.

Visual Result

Tentacles appear as a radial fringe of colored lines immediately around each jellyfish body. From the isometric top-down camera angle, downward-hanging tentacles are foreshortened — appears as a small colored cluster rather than long dangling appendages. Correctly attached to characters, correctly auto-scaled, correctly team-colored.

---

2026-04-04 — Tilt Control in Rebind UI + In-Match Menu Hints

What Changed

Three pending gaps from the 2026-03-23 manual tilt session completed:

1. Tilt (right stick) added to all rebind UIs

2. Auto-tilt toggle wiring completed (PlayerMenuController + PlayerSettingsPanel)

3. Per-player menu hint text added to in-match HUD and settings panel

---

Tilt in Rebind UIs

Tilt was fully implemented in the input system but never surfaced to players as a rebindable control.

Files changed

InputRebindMenuUI.cs

  • Added "Tilt" to rebindableActions. The UGUI lobby rebind panel now shows a Tilt row for controller players. Keyboard players see no row (Tilt has no keyboard binding — skipped automatically by GetBindingIndexForControlScheme returning -1).

PlayerSettingsPanel.cs

  • Added "Tilt" to rebindableActions. Same behavior in the in-game settings panel.

InputRebindUIController.cs (UI Toolkit lobby panel)

  • BindingNames / ActionMappings: added "tilt" / "Tilt" at index 6
  • ControllerPresetBindings: added tilt as 4th element per preset:
  • L-Stick + Buttons → rightStick
  • L-Stick + Triggers → rightStick
  • R-Stick + Buttons → leftStick (rightStick already used for Move in this preset)
  • D-Pad + Buttons → rightStick
  • ApplyPreset() controller branch: calls ApplyControllerMovePreset(tiltAction, presetPaths[3]) — correct because Tilt is Value/Vector2 (same structure as Move)

---

Auto-Tilt Toggle Wiring

PlayerMenuController and PlayerSettingsPanel code was complete since 2026-03-23. The missing piece was editor wiring. The existing PlayerMenuSetupWizard (Tools > Player Menu Setup) confirmed everything was already in place from a prior run:

Toggle behavior: visible only for controller players (keyboard always uses auto-tilt; hidden for them). Players access it via MenuOpen (Start/Escape) → Settings panel.

---

Per-Player Menu Hint Text

New UI prompt system so players know how to open/close the per-player settings panel during a match.

`PlayerMenuController.cs`

  • Added [SerializeField] TextMeshProUGUI menuHintText
  • Start(): sets initial text, subscribes to PlayerSetup.OnInputModeChanged
  • UpdateHintText() sets text based on current state:

| State | Controller | Keyboard |

|---|---|---|

| Menu closed | START — Settings | ESC — Settings |

| Menu open | START — Close Settings | ESC — Close Settings |

| Waiting for controller | Waiting for controller… | — |

  • Fires on OpenMenu(), CloseMenu(), and any device change event

`PlayerSettingsPanel.cs`

  • Added [SerializeField] TextMeshProUGUI instructionText
  • Set in Show() based on player's input mode:
  • Controller: START or B — Close
  • Keyboard: ESC — Close

Unity wiring — `MenuHintWiringWizard.cs` (new editor script)

Runs via Tools > Wire Menu Hint Text. One-shot, rerunnable.

  • PlayerPrefab: Created MenuHintText TMP child under BarCanvas, anchored below the health bar. Font size 3 (world-space canvas), 65% opacity white, centre-aligned. Wired to PlayerMenuController.menuHintText.
  • LobbyScene / PlayerSettingsPanelCanvas/PanelRoot: Created InstructionText TMP anchored to panel bottom, font size 13, grey. Wired to PlayerSettingsPanel.instructionText.
  • LobbyScene saved.

Players can now manually tilt the jellyfish bowl using the right stick, with the tilt range expanded significantly for more expressive play. Blood is stickier and less likely to spill during movement, and carrier mode slows movement slightly to reduce inertia. These changes make carrying blood feel more deliberate.

The settings system is now fully working. All sliders — volume, resolution, graphics quality, camera shake — are functional in both the main menu and during matches. The SFX volume slider previously had no effect because audio sources weren't wired to the mixer. Fixed. Camera shake toggle now works too.

Several memory leaks were resolved that caused performance to degrade over long sessions. Standalone build crashes were fixed — missing shaders caused visual elements not to spawn in builds at all. Unglamorous sessions, but necessary ones.

Raw session notes

Mar 22 — Memory leaks & outlast phase

Fixed duplicate event subscriptions, mesh asset leaks, static dictionary never cleared between matches, anonymous lambda closures keeping dead objects alive. Added OutlastPanel with 4× time scale drain.

Mar 23 — Tilt system, carrier speed, controller menus

Manual tilt via right stick (75° range). Carrier mode 80% speed. ObiFluid viscosity increased. Per-player lobby settings panel via Start/Escape.

Mar 26 — Build fixes, pause menu

Fixed standalone crashes: missing shaders, GPU memory leak on restart, main menu buttons. Added Settings to Pause and Main Menu.

Mar 27–28 — Full settings system & verification

Audio/video/graphics/gameplay settings, persisted via PlayerPrefs. SFX routing fixed. Camera shake toggle fixed. Resolution default fixed.

Every action in a match now has sound. Countdown ticks, floor scroll, match start and end, player death, form transform, evolution, altar filling, blood hitting the floor, UI buttons — all wired in a single session. The game went from silent to fully voiced in one pass.

Visual feedback improved significantly. Glowing rings appear on floor tiles that have a blood emitter underneath, making it easier to know where blood will come from. Altars now turn red from the bottom up as they fill, so you can read altar progress at a glance without checking a number. Blood emitter fluid was tuned to behave more like blood.

Jellyfish creatures got animated tentacles. Player 4 controller disconnect and reconnect handling was fixed. Blood leaking through floors due to collider issues was resolved.

Raw session notes

Mar 15–16 — Visuals & controller fix

Emitter indicator glowing rings on tile surfaces. Jellyfish animated tentacles. Pouring mechanic first visual pass. Player 4 controller hotplug fix.

Mar 18–19 — Blood & altar

Blood emitter fluid tuning. Altar fill visual shader (URP HLSL, red fill bottom-up). Obi fluid collider leak fix.

Mar 20 — Audio system

Full audio pass: 7 components, 18+ events covered. Countdown, scroll alarm, evolution sting, death sound, form swap, altar sounds, floor impact, UI audio.

Mar 21 — Player 4 controller

Controller disconnect/reconnect handling fixed for Player 4.

All 13 core game systems were completed and wired up in Unity — the game could run a full match end-to-end for the first time. Spawn system, countdown, match flow, win/loss detection, onboarding overlays, pause menu, terminal phase where eliminated teams watch their bar drain out. Getting all of this connected was the majority of this period.

The match wasn't actually starting after all that. Countdown never appeared, blood never emitted. Root cause was a spawn timing issue on the first frame — fixed by moving spawn dispatch to a later update pass. Floor boundary walls were added, and floors without players now fade out so the active floor is easier to read. Pillars between the camera and players turn semi-transparent.

Raw session notes

Mar 5–10 — Alpha systems complete & scene wiring

All 13 systems implemented and wired in Unity editor. First full match runnable.

Mar 11 — Floor visibility & boundary walls

Floor fades when no players present. Boundary walls added to arena edges.

Mar 14 — Match start fix & pillar occlusion

Fixed match not starting (spawn timing on first frame). Added pillar transparency when occluding players.

Players now have full control over their input setup — custom key bindings for keyboard and controller, with preset layouts (WASD, Arrows, IJKL, Numpad), conflict detection so two players can't claim the same keys, and swap confirmation when rebinding overlapping controls. Any combination of keyboards and controllers should just work.

The core arena mechanic was built: three floors stacked vertically, scrolling downward as altars are consumed. Blood emitters activate and deactivate by floor role. A vertical hazard descends from above. Visual proportions were overhauled after early testing showed everything looking wrong — platforms too thin, camera too far out, pillars piercing through multiple floors. All fixed.

The evolution bar was redesigned: instead of resetting when you evolve, the bar grows 30% wider and changes colour. Evolution feels like gaining something rather than starting over.

Raw session notes

Jan 22–26 — Input rebinding system

Full controller/keyboard rebind UI. Shared keyboard/controller support. Presets, conflict detection, swap confirmation, startup validation.

Feb 5 — Scrolling floor system

3-floor stacked arena. Chunk grid, gap generation, emitter/altar placement, BFS path validation. Scroll trigger on altar consumption. Vertical hazard.

Feb 14 — Pillar fix & visual overhaul

Fixed pillars piercing all floors. Floor spacing 10→5 units. Chunk thickness 0.2→1.0. Arena now looks proportional.

Feb 21 — Rename & alpha plan

Form-toggle action renamed across codebase. 13-system alpha implementation plan written with dependency tiers.

Feb 26 — Evolution bar redesign

Bar grows 30% wider on evolution instead of resetting. Tier accent colours added.

Why I automated my devlogs

The honest answer is that I kept not writing them. Not because nothing was happening — the game is being worked on almost every day — but because after a session I just want to stop. The last thing I want to do is write about what I just spent three hours doing.

So the devlogs never got written, and from the outside it probably looked like the game was dead. It wasn't. It just had no voice.

The automated log isn't a replacement for real devlogs. It's a proof-of-life signal. A way for anyone following the project to see that work is happening, even when I'm too tired to say so myself.