Spaces:
Sleeping
Sleeping
Commit Β·
35d9271
1
Parent(s): 702bac2
docs: game-view UI polish implementation plan (transcript/voices/color/feedback)
Browse files
docs/superpowers/plans/2026-06-15-game-ui-polish.md
ADDED
|
@@ -0,0 +1,408 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Game-View UI Polish Implementation Plan
|
| 2 |
+
|
| 3 |
+
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development or superpowers:executing-plans. Steps use checkbox (`- [ ]`).
|
| 4 |
+
|
| 5 |
+
**Goal:** Polish the in-game UI for playability β a readable fading transcript with a clear two-voice hierarchy, an amber "memory" / red "cruelty" color language, and whispered in-voice feedback for capture/recall/recover.
|
| 6 |
+
|
| 7 |
+
**Architecture:** Mostly CSS in `styles.css` Β§26 plus two small seams that ride the **existing per-turn marker pattern** (the `.tone-now` β `applyTone()` mechanism): `render_entity` emits a `.cue-now[data-cue][data-seq]` marker, and a new `applyCue()` in `_HEAD_JS` (polled by the existing `wire()` loop) pulses the right drawer chip, drops a transient whisper, and tags the recall line amber. No chat engine / recall / ending / voice changes.
|
| 8 |
+
|
| 9 |
+
**Tech Stack:** Gradio 6 (HTML+CSS+`_HEAD_JS`), pure CSS, pytest for the one pure helper (`render_entity` marker).
|
| 10 |
+
|
| 11 |
+
**Reference spec:** `docs/superpowers/specs/2026-06-15-game-ui-polish-design.md`. **Decisions:** transcript=B, voices=V1, color=B, feedback=B.
|
| 12 |
+
|
| 13 |
+
**Verification:** visuals by eye (headless unreliable β CLAUDE.md); `import app` + `pytest -q` stay green (262). Gradio chatbot internal class names vary β target `.bot`/`.user` (load-bearing, already used) and verify `.message-row`/`.prose` against the running app.
|
| 14 |
+
|
| 15 |
+
---
|
| 16 |
+
|
| 17 |
+
### Task 1: Color var β the amber "memory" accent
|
| 18 |
+
|
| 19 |
+
**Files:** Modify `styles.css` (Β§1 `:root`, ~line 12)
|
| 20 |
+
|
| 21 |
+
- [ ] **Step 1: Add the memory accent var**
|
| 22 |
+
|
| 23 |
+
In `:root` (after the existing accent vars near line 20), add:
|
| 24 |
+
|
| 25 |
+
```css
|
| 26 |
+
/* memory/humanity accent (recall line, recovered items, feedback pulses) β
|
| 27 |
+
distinct from the dried-blood red reserved for cruelty/bad-ending */
|
| 28 |
+
--mem-accent: #e0b283;
|
| 29 |
+
--mem-glow: rgba(200,130,70,0.55);
|
| 30 |
+
```
|
| 31 |
+
|
| 32 |
+
- [ ] **Step 2: Verify import**
|
| 33 |
+
|
| 34 |
+
Run: `./.venv/Scripts/python -c "import app; print('ok')"` β `ok` (CSS isn't parsed by Python; this just confirms nothing else broke).
|
| 35 |
+
|
| 36 |
+
- [ ] **Step 3: Commit**
|
| 37 |
+
|
| 38 |
+
```bash
|
| 39 |
+
git add styles.css
|
| 40 |
+
git commit -m "style(game): add --mem-accent amber var (memory/humanity)"
|
| 41 |
+
```
|
| 42 |
+
|
| 43 |
+
---
|
| 44 |
+
|
| 45 |
+
### Task 2: Transcript fade + two-voice hierarchy (B + V1)
|
| 46 |
+
|
| 47 |
+
**Files:** Modify `styles.css` Β§26 (the `#game-dialogue` chat rules, ~lines 1669-1691)
|
| 48 |
+
|
| 49 |
+
Make the conversation read as a fading transcript: last lines visible, older ones dimmer; the child italic & unlabeled, the player upright with a `you` typewriter tag, centered (never scattered). Recall lines turn amber.
|
| 50 |
+
|
| 51 |
+
- [ ] **Step 1: Replace the `#game-dialogue` chat block**
|
| 52 |
+
|
| 53 |
+
Replace the current rules from `#game-dialogue .chatbot {` through the `.user` rule (the block ending at the line that sets `.user ... color:#b9b2cc`) with:
|
| 54 |
+
|
| 55 |
+
```css
|
| 56 |
+
#game-dialogue .chatbot {
|
| 57 |
+
width: 100% !important;
|
| 58 |
+
max-height: 160px !important;
|
| 59 |
+
background: transparent !important;
|
| 60 |
+
border: none !important;
|
| 61 |
+
border-image: none !important;
|
| 62 |
+
box-shadow: none !important;
|
| 63 |
+
backdrop-filter: none !important;
|
| 64 |
+
padding: 0 !important;
|
| 65 |
+
overflow: hidden; /* old lines fade out, no scrollbar */
|
| 66 |
+
}
|
| 67 |
+
/* transparent rows, centered text β kill Gradio's bubble chrome */
|
| 68 |
+
#game-dialogue .message-row,
|
| 69 |
+
#game-dialogue .message,
|
| 70 |
+
#game-dialogue .bubble {
|
| 71 |
+
background: transparent !important; border: none !important;
|
| 72 |
+
box-shadow: none !important; text-align: center !important;
|
| 73 |
+
padding: 2px 0 !important; margin: 0 !important;
|
| 74 |
+
}
|
| 75 |
+
/* fade-by-recency: newest crisp, older dimmer, 4th-back and beyond hidden.
|
| 76 |
+
(Gradio wraps each message in a .message-row β verify this selector against
|
| 77 |
+
the running app; if rows are .message instead, swap the selector.) */
|
| 78 |
+
#game-dialogue .message-row { opacity: 0.38; transition: opacity 0.4s ease; }
|
| 79 |
+
#game-dialogue .message-row:nth-last-child(2) { opacity: 0.6; }
|
| 80 |
+
#game-dialogue .message-row:nth-last-child(1) { opacity: 1; }
|
| 81 |
+
#game-dialogue .message-row:nth-last-child(n+4) { display: none; }
|
| 82 |
+
|
| 83 |
+
/* the child β italic, bright, unlabeled (it owns the scene) */
|
| 84 |
+
#game-dialogue .bot, #game-dialogue .bot .prose, #game-dialogue .bot p {
|
| 85 |
+
font-style: italic !important; font-size: 1.2rem !important; line-height: 1.55 !important;
|
| 86 |
+
color: #ece6f4 !important; text-align: center !important;
|
| 87 |
+
text-shadow: 0 2px 16px #000 !important;
|
| 88 |
+
}
|
| 89 |
+
/* you β upright, muted, with a small typewriter tag */
|
| 90 |
+
#game-dialogue .user, #game-dialogue .user .prose, #game-dialogue .user p {
|
| 91 |
+
font-style: normal !important; font-size: 1.0rem !important;
|
| 92 |
+
color: #aaa2be !important; text-align: center !important; background: transparent !important;
|
| 93 |
+
}
|
| 94 |
+
#game-dialogue .user .prose::before {
|
| 95 |
+
content: 'you ';
|
| 96 |
+
font-family: 'Special Elite', monospace;
|
| 97 |
+
font-size: 0.6rem; letter-spacing: 0.18em; text-transform: uppercase;
|
| 98 |
+
color: #6f6590; margin-right: 7px;
|
| 99 |
+
}
|
| 100 |
+
/* a recall line (it claims your memory) β JS tags the last .bot row .recall-line */
|
| 101 |
+
#game-dialogue .bot.recall-line, #game-dialogue .bot.recall-line .prose,
|
| 102 |
+
#game-dialogue .bot.recall-line p {
|
| 103 |
+
color: var(--mem-accent) !important;
|
| 104 |
+
text-shadow: 0 0 20px var(--mem-glow) !important;
|
| 105 |
+
}
|
| 106 |
+
```
|
| 107 |
+
|
| 108 |
+
- [ ] **Step 2: Live-verify (user, by eye)**
|
| 109 |
+
|
| 110 |
+
Run `.venv/Scripts/python app.py`, enter the game, send 2-3 messages. Confirm: the child's lines are italic/centered, your lines are upright/centered with a `you` tag (NOT scattered left/right), older lines dim and only ~3 show. If lines don't fade, the row selector is wrong β change `.message-row` to the actual per-message wrapper class (inspect element) and re-verify.
|
| 111 |
+
|
| 112 |
+
- [ ] **Step 3: Commit**
|
| 113 |
+
|
| 114 |
+
```bash
|
| 115 |
+
git add styles.css
|
| 116 |
+
git commit -m "style(game): fading transcript + two-voice hierarchy (B + V1)"
|
| 117 |
+
```
|
| 118 |
+
|
| 119 |
+
---
|
| 120 |
+
|
| 121 |
+
### Task 3: Recovered items use the amber accent
|
| 122 |
+
|
| 123 |
+
**Files:** Modify `styles.css` Β§26 (add a rule near the drawer/recovered styles)
|
| 124 |
+
|
| 125 |
+
The "What it remembers" items are the child's recovered humanity β amber, per the color decision.
|
| 126 |
+
|
| 127 |
+
- [ ] **Step 1: Add the recovered-item color rule**
|
| 128 |
+
|
| 129 |
+
Append in Β§26 (near the `.game-drawer` rules):
|
| 130 |
+
|
| 131 |
+
```css
|
| 132 |
+
/* the child's recovered memories read in the warm memory accent */
|
| 133 |
+
.game-drawer .recovered-item {
|
| 134 |
+
color: var(--mem-accent) !important;
|
| 135 |
+
border-left-color: rgba(200,130,70,0.5) !important;
|
| 136 |
+
}
|
| 137 |
+
```
|
| 138 |
+
|
| 139 |
+
- [ ] **Step 2: Commit**
|
| 140 |
+
|
| 141 |
+
```bash
|
| 142 |
+
git add styles.css
|
| 143 |
+
git commit -m "style(game): recovered items in the amber memory accent"
|
| 144 |
+
```
|
| 145 |
+
|
| 146 |
+
---
|
| 147 |
+
|
| 148 |
+
### Task 4: `render_entity` emits a per-turn cue marker
|
| 149 |
+
|
| 150 |
+
**Files:** Modify `render.py` (`render_entity` signature + return, ~lines 79-313); Test: `tests/test_render.py`
|
| 151 |
+
|
| 152 |
+
Add an optional `cue` that surfaces a hidden marker (mirroring `.tone-now`) so the head-JS can fire the right feedback once per turn.
|
| 153 |
+
|
| 154 |
+
- [ ] **Step 1: Write the failing test**
|
| 155 |
+
|
| 156 |
+
In `tests/test_render.py`:
|
| 157 |
+
|
| 158 |
+
```python
|
| 159 |
+
def test_entity_cue_marker():
|
| 160 |
+
from render import render_entity
|
| 161 |
+
assert 'data-cue="recall"' in render_entity(60, cue="recall", seq=3)
|
| 162 |
+
assert 'data-seq="3"' in render_entity(60, cue="recall", seq=3)
|
| 163 |
+
# no cue β no marker
|
| 164 |
+
assert 'cue-now' not in render_entity(20)
|
| 165 |
+
```
|
| 166 |
+
|
| 167 |
+
- [ ] **Step 2: Run it (fails)**
|
| 168 |
+
|
| 169 |
+
Run: `./.venv/Scripts/python -m pytest tests/test_render.py::test_entity_cue_marker -v` β FAIL (unexpected `cue` kwarg).
|
| 170 |
+
|
| 171 |
+
- [ ] **Step 3: Add the `cue` param**
|
| 172 |
+
|
| 173 |
+
Change the signature:
|
| 174 |
+
|
| 175 |
+
```python
|
| 176 |
+
def render_entity(affinity: int, mode: str = "idle", seq: int = 0,
|
| 177 |
+
tone: int = 0, cue: str = "") -> str:
|
| 178 |
+
```
|
| 179 |
+
|
| 180 |
+
- [ ] **Step 4: Emit the marker in the return**
|
| 181 |
+
|
| 182 |
+
In the final `return` (the block starting with the `tone-now` span), prepend a cue marker when `cue` is set. Replace the return with:
|
| 183 |
+
|
| 184 |
+
```python
|
| 185 |
+
tone_token = tone_class.split("-", 1)[1] if "-" in tone_class else "neutral"
|
| 186 |
+
cue_mark = (f'<span class="cue-now" data-cue="{cue}" data-seq="{seq}" '
|
| 187 |
+
'style="display:none"></span>') if cue else ""
|
| 188 |
+
return (
|
| 189 |
+
cue_mark
|
| 190 |
+
+ f'<span class="tone-now" data-tone="{tone_token}" style="display:none"></span>'
|
| 191 |
+
f'<div class="entity-scene {tone_class}">'
|
| 192 |
+
f'{figure}{flash}{tint}'
|
| 193 |
+
'</div>'
|
| 194 |
+
f'{ghost}'
|
| 195 |
+
)
|
| 196 |
+
```
|
| 197 |
+
|
| 198 |
+
- [ ] **Step 5: Run the test + full render suite**
|
| 199 |
+
|
| 200 |
+
Run: `./.venv/Scripts/python -m pytest tests/test_render.py -q` β PASS.
|
| 201 |
+
|
| 202 |
+
- [ ] **Step 6: Commit**
|
| 203 |
+
|
| 204 |
+
```bash
|
| 205 |
+
git add render.py tests/test_render.py
|
| 206 |
+
git commit -m "feat(entity): optional per-turn cue marker (capture/recall/recover)"
|
| 207 |
+
```
|
| 208 |
+
|
| 209 |
+
---
|
| 210 |
+
|
| 211 |
+
### Task 5: `chat()` computes and passes the cue
|
| 212 |
+
|
| 213 |
+
**Files:** Modify `app.py` (the main turn body + yield, ~lines 923-1034)
|
| 214 |
+
|
| 215 |
+
Surface which beat happened this turn so render_entity can mark it. Precedence: recall > recover > capture.
|
| 216 |
+
|
| 217 |
+
- [ ] **Step 1: Capture the fragment count BEFORE it can increment**
|
| 218 |
+
|
| 219 |
+
Find `do_recall, recall_memory = should_recall(state)` (~line 923). Immediately AFTER it, add:
|
| 220 |
+
|
| 221 |
+
```python
|
| 222 |
+
frag_before = state.get("fragments_told", 0)
|
| 223 |
+
```
|
| 224 |
+
|
| 225 |
+
- [ ] **Step 2: Compute the cue just before the final yield**
|
| 226 |
+
|
| 227 |
+
Find `just_claimed = recall_memory if (do_recall and recall_memory) else None` (~line 1020). Immediately AFTER it, add:
|
| 228 |
+
|
| 229 |
+
```python
|
| 230 |
+
recovered_now = state.get("fragments_told", 0) > frag_before
|
| 231 |
+
cue = ("recall" if (do_recall and recall_memory)
|
| 232 |
+
else "recover" if recovered_now
|
| 233 |
+
else "capture" if captured else "")
|
| 234 |
+
```
|
| 235 |
+
|
| 236 |
+
- [ ] **Step 3: Pass the cue into the final `render_entity`**
|
| 237 |
+
|
| 238 |
+
In the final yield, change:
|
| 239 |
+
|
| 240 |
+
```python
|
| 241 |
+
render_entity(state["affinity"], mode, seq=state["turn"],
|
| 242 |
+
tone=state.get("tone", 0)),
|
| 243 |
+
```
|
| 244 |
+
|
| 245 |
+
to:
|
| 246 |
+
|
| 247 |
+
```python
|
| 248 |
+
render_entity(state["affinity"], mode, seq=state["turn"],
|
| 249 |
+
tone=state.get("tone", 0), cue=cue),
|
| 250 |
+
```
|
| 251 |
+
|
| 252 |
+
- [ ] **Step 4: Verify import + suite**
|
| 253 |
+
|
| 254 |
+
Run: `./.venv/Scripts/python -c "import app; print('ok')"` β `ok`; then `./.venv/Scripts/python -m pytest -q` β 263 passed (262 + the new render test).
|
| 255 |
+
|
| 256 |
+
- [ ] **Step 5: Commit**
|
| 257 |
+
|
| 258 |
+
```bash
|
| 259 |
+
git add app.py
|
| 260 |
+
git commit -m "feat(game): chat() surfaces the per-turn cue (recall/recover/capture)"
|
| 261 |
+
```
|
| 262 |
+
|
| 263 |
+
---
|
| 264 |
+
|
| 265 |
+
### Task 6: `applyCue()` β drawer pulse, whisper, recall-line tag
|
| 266 |
+
|
| 267 |
+
**Files:** Modify `app.py` (`_HEAD_JS`: add `applyCue()` + call it in `wire()`, ~lines 428-442)
|
| 268 |
+
|
| 269 |
+
Mirror `applyTone()`: read the marker, fire once per `data-seq`, pulse the matching chip, drop a transient whisper, and amber-tag the recall line.
|
| 270 |
+
|
| 271 |
+
- [ ] **Step 1: Add `applyCue()` after `applyTone()`**
|
| 272 |
+
|
| 273 |
+
Immediately after the `applyTone()` function (it ends at line ~439, the closing `}` before `function wire()`), insert:
|
| 274 |
+
|
| 275 |
+
```javascript
|
| 276 |
+
// once-per-turn feedback: pulse the relevant drawer chip, drop a whisper,
|
| 277 |
+
// amber-tag the recall line. Rides the same marker pattern as applyTone.
|
| 278 |
+
var CUE_WHISPER = { capture: '\\u2014 it keeps this \\u2014',
|
| 279 |
+
recover: '\\u2014 it remembers \\u2014' };
|
| 280 |
+
function applyCue() {
|
| 281 |
+
var gv = document.getElementById('game-view');
|
| 282 |
+
var mark = document.querySelector('#game-view .cue-now[data-cue]');
|
| 283 |
+
if (!gv || !mark) return;
|
| 284 |
+
var seq = mark.getAttribute('data-seq');
|
| 285 |
+
if (gv.dataset.cueSeq === seq) return; // already fired this turn
|
| 286 |
+
gv.dataset.cueSeq = seq;
|
| 287 |
+
var cue = mark.getAttribute('data-cue');
|
| 288 |
+
var rightSide = (cue === 'recover');
|
| 289 |
+
var drawer = document.getElementById(rightSide ? 'drawer-right' : 'drawer-left');
|
| 290 |
+
if (drawer) {
|
| 291 |
+
drawer.classList.add('pulse');
|
| 292 |
+
setTimeout(function () { drawer.classList.remove('pulse'); }, 2500);
|
| 293 |
+
}
|
| 294 |
+
if (cue === 'recall') {
|
| 295 |
+
var bots = document.querySelectorAll('#game-dialogue .bot');
|
| 296 |
+
if (bots.length) bots[bots.length - 1].classList.add('recall-line');
|
| 297 |
+
}
|
| 298 |
+
var text = CUE_WHISPER[cue];
|
| 299 |
+
if (text && drawer) {
|
| 300 |
+
var w = document.createElement('div');
|
| 301 |
+
w.className = 'cue-whisper';
|
| 302 |
+
w.textContent = text;
|
| 303 |
+
var r = drawer.getBoundingClientRect();
|
| 304 |
+
w.style.left = r.left + 'px';
|
| 305 |
+
w.style.top = (r.bottom + 6) + 'px';
|
| 306 |
+
document.body.appendChild(w);
|
| 307 |
+
setTimeout(function () { w.classList.add('out'); }, 60);
|
| 308 |
+
setTimeout(function () { w.remove(); }, 2800);
|
| 309 |
+
}
|
| 310 |
+
}
|
| 311 |
+
```
|
| 312 |
+
|
| 313 |
+
- [ ] **Step 2: Call it from `wire()`**
|
| 314 |
+
|
| 315 |
+
In `wire()`, the line `applyTone();` (~line 442) β add `applyCue();` right after it:
|
| 316 |
+
|
| 317 |
+
```javascript
|
| 318 |
+
function wire() {
|
| 319 |
+
applyTone();
|
| 320 |
+
applyCue();
|
| 321 |
+
```
|
| 322 |
+
|
| 323 |
+
- [ ] **Step 3: Verify import + that the JS shipped**
|
| 324 |
+
|
| 325 |
+
Run: `./.venv/Scripts/python -c "import app; print('ok')"` β `ok`. (The `\\u2014` escapes are em-dashes; keep them escaped β this file has hit UTF-8 mangling before, see CLAUDE.md.)
|
| 326 |
+
|
| 327 |
+
- [ ] **Step 4: Commit**
|
| 328 |
+
|
| 329 |
+
```bash
|
| 330 |
+
git add app.py
|
| 331 |
+
git commit -m "feat(game): applyCue β chip pulse + in-voice whisper + recall-line tag"
|
| 332 |
+
```
|
| 333 |
+
|
| 334 |
+
---
|
| 335 |
+
|
| 336 |
+
### Task 7: CSS β chip pulse + whisper styling
|
| 337 |
+
|
| 338 |
+
**Files:** Modify `styles.css` Β§26
|
| 339 |
+
|
| 340 |
+
- [ ] **Step 1: Add the pulse + whisper rules**
|
| 341 |
+
|
| 342 |
+
Append in Β§26:
|
| 343 |
+
|
| 344 |
+
```css
|
| 345 |
+
/* feedback: the drawer chip breathes amber when its collection changes */
|
| 346 |
+
.game-drawer.pulse {
|
| 347 |
+
border-color: rgba(200,150,80,0.6) !important;
|
| 348 |
+
box-shadow: 0 0 18px rgba(200,150,80,0.45) !important;
|
| 349 |
+
animation: chip-pulse 1.1s ease-in-out 2;
|
| 350 |
+
}
|
| 351 |
+
.game-drawer.pulse .drawer-head { color: var(--mem-accent) !important; }
|
| 352 |
+
@keyframes chip-pulse {
|
| 353 |
+
0%, 100% { box-shadow: 0 0 10px rgba(200,150,80,0.25); }
|
| 354 |
+
50% { box-shadow: 0 0 22px rgba(200,150,80,0.55); }
|
| 355 |
+
}
|
| 356 |
+
|
| 357 |
+
/* the in-voice whisper that breathes next to the chip, then fades */
|
| 358 |
+
.cue-whisper {
|
| 359 |
+
position: fixed; z-index: 60; pointer-events: none;
|
| 360 |
+
font-family: 'Special Elite', monospace;
|
| 361 |
+
font-size: 0.62rem; letter-spacing: 0.14em;
|
| 362 |
+
color: var(--mem-accent); text-shadow: 0 0 10px var(--mem-glow);
|
| 363 |
+
opacity: 0; transform: translateY(-3px);
|
| 364 |
+
transition: opacity 0.5s ease, transform 0.5s ease;
|
| 365 |
+
}
|
| 366 |
+
.cue-whisper.out { opacity: 0; transform: translateY(-8px); }
|
| 367 |
+
.cue-whisper { animation: whisper-in 0.5s ease forwards; }
|
| 368 |
+
@keyframes whisper-in { from { opacity: 0; } 40% { opacity: 0.9; } to { opacity: 0.9; } }
|
| 369 |
+
```
|
| 370 |
+
|
| 371 |
+
(The `.out` class drives the fade-away after ~2.5 s; `whisper-in` brings it in.)
|
| 372 |
+
|
| 373 |
+
- [ ] **Step 2: Reduced-motion safety**
|
| 374 |
+
|
| 375 |
+
In the existing `@media (prefers-reduced-motion: reduce)` block (near the end of Β§26), add:
|
| 376 |
+
|
| 377 |
+
```css
|
| 378 |
+
.game-drawer.pulse { animation: none !important; }
|
| 379 |
+
.cue-whisper { animation: none !important; opacity: 0.9 !important; }
|
| 380 |
+
```
|
| 381 |
+
|
| 382 |
+
- [ ] **Step 3: Commit**
|
| 383 |
+
|
| 384 |
+
```bash
|
| 385 |
+
git add styles.css
|
| 386 |
+
git commit -m "style(game): chip pulse + in-voice whisper feedback styling"
|
| 387 |
+
```
|
| 388 |
+
|
| 389 |
+
---
|
| 390 |
+
|
| 391 |
+
### Task 8: Full suite + live verification
|
| 392 |
+
|
| 393 |
+
- [ ] **Step 1:** Run `./.venv/Scripts/python -m pytest -q` β all green (263). Fix any breakage.
|
| 394 |
+
- [ ] **Step 2 (user, by eye + ear, `.venv-tts` + Ollama):** play a few turns and confirm:
|
| 395 |
+
- Conversation reads as a centered fading transcript; child italic, your lines tagged `you`, never scattered; only ~3 lines show.
|
| 396 |
+
- On a captured memory: the **Treasure** chip pulses amber + "β it keeps this β" breathes and fades.
|
| 397 |
+
- On a recall: the child's line turns amber + the Treasure item shows claimed.
|
| 398 |
+
- On a recovered fragment: the **What it remembers** chip pulses + "β it remembers β".
|
| 399 |
+
- Recovered items render amber; cruelty/bad-ending still red.
|
| 400 |
+
- [ ] **Step 3:** Commit any tuning.
|
| 401 |
+
|
| 402 |
+
---
|
| 403 |
+
|
| 404 |
+
## Self-Review
|
| 405 |
+
- **Spec coverage:** transcript B (Task 2) β
; voices V1 β `you` tag + child italic + recall amber (Tasks 2, 6) β
; color B β amber memory var + recall + recovered + pulses, red untouched (Tasks 1, 2, 3, 7) β
; feedback B β thinking unchanged, capture/recover whisper + chip pulse, recall glow (Tasks 4-7) β
; bond unchanged β
; no engine changes β
.
|
| 406 |
+
- **Placeholder scan:** every code step shows full code; no TBDs.
|
| 407 |
+
- **Type/seam consistency:** `cue` kwarg added in Task 4 is passed in Task 5; the `.cue-now[data-cue][data-seq]` marker (Task 4) is read by `applyCue()` (Task 6); `--mem-accent`/`--mem-glow` (Task 1) used in Tasks 2/3/7; `.recall-line` tagged in Task 6, styled in Task 2; `.pulse` added in Task 6, styled in Task 7; `#drawer-left`/`#drawer-right` are existing ids (confirmed).
|
| 408 |
+
- **Fragility flagged:** the `.message-row` fade selector and `.user .prose::before` tag are Gradio-version-dependent β Task 2 Step 2 verifies them live; `.bot`/`.user` are the load-bearing classes already in use.
|