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 meshCollider → private 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 step —
Resources.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 event —
EnterTerminal() 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 01…SUBJECT 04
- [x] 1.5 Start button →
[ COMMENCE EXPERIMENT ]
- [x] 1.6 Countdown sequence →
EXPERIMENT INITIATING IN 3/2/1 → EXPERIMENT 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.cs — CanvasGroupFade wired on built panel root for fade-in/out hooks
Assets/ProtoV2/Scripts/ProgressionManager.cs — HandleAltarConsumed 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.cs — CreateFloatingRewardPrefab region removed; all FloatingRewardIndicator references deleted
Assets/ProtoV2/Prefabs/FloorSystem/Altar.prefab — AltarCompletionFeedback 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.cs — time-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.cs — Phase { 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.