# Handoff: Hundred Line PLM Redesign — V2 (Vilmonic Cool)

## Overview

The PLM (Play-Logging Manager) is a companion utility that runs alongside the *Hundred Line* game inside a wider browser. It connects to the game's memory and surfaces, in real time:

- The current in-game day, route, and dialogue address
- The currently-spoken line in **two** languages (EN + JP) side-by-side
- The active character sprite (with a button to flip which side it shows on)
- Per-language audio replay (keyboard-driven)

This handoff covers the **V2 "Vilmonic Cool"** direction the user picked — chunky beveled pixel-window chrome inspired by the indie game *Vilmonic*, sitting in front of a Bayer-dithered cyan/teal/purple gradient field, with a procedurally-rendered bacteria-like sprite placeholder.

## About the Design Files

The HTML/JSX files in `source/` are **design references**, not production code. They are inline-Babel React prototypes built to demonstrate the intended look, layout, palette, motion, and behavior of the redesigned PLM.

The task is to **recreate this design inside the existing PLM codebase's environment** (whatever framework it currently uses — likely a desktop-app shell like Electron/Tauri/PyQt+WebView given the screenshots, or a plain HTML/JS overlay) using its established patterns. Do **not** ship the JSX prototypes directly — port them.

If no framework is established, plain HTML + Canvas + a thin React/Preact layer would be a reasonable choice; the dithered background and the bacteria sprite are both 2D Canvas renders so they'll move cleanly to any environment that supports `<canvas>`.

## Fidelity

**High-fidelity.** Colors, spacing, typography, bevel widths, and motion are all final. Recreate pixel-perfectly.

That said, the current "bacteria" sprite is a **placeholder** standing in for the real game-character sprite the PLM will inject once it reads the right address out of game memory. The placeholder demonstrates:

- The expected **visible area** in the sprite panel (≈ 240×580 inside the bevel)
- The **frame chrome** that should wrap whatever sprite gets injected
- The **idle motion** vibe — the real sprite is allowed to be still; the placeholder moves so the panel doesn't feel inert before connection.

When wiring the real sprite, drop the `BacteriaSprite` canvas, render the game sprite at native pixel size with `image-rendering: pixelated` and an integer scale, and keep the surrounding `BacteriaPanel` chrome intact.

## Screen: PLM Main Window

There is one screen. Default size in the prototype is **1100 × 820**, but the window is fluid — it should fill whatever bounding box the wider browser gives it.

### Layout (top → bottom)

```
┌──────────────────────────────────────────────┐
│  outer dithered cyan/teal/purple field       │  ← 28px padding all around
│  ┌────────────────────────────────────────┐  │
│  │ TITLE BAR  ●●●  PLM · HUNDRED LINE …   │  │
│  ├──────────┬─────────────────────────────┤  │
│  │ DAY 100  │ ROUTE  ADDR    SPEAKER ──── │  │  ← day banner
│  ├──────────┴────────────────────┬────────┤  │
│  │                               │        │  │
│  │   EN line                     │  ┌──┐  │  │
│  │   JP line                     │  │  │  │  │  ← body grid, sprite right
│  │                               │  │SP│  │  │
│  │   keyboard hints              │  └──┘  │  │
│  └───────────────────────────────┴────────┘  │
└──────────────────────────────────────────────┘
```

The sprite column flips to the **left** when the user clicks the `⇆ SPRITE` chip in the title bar (or hits `S`).

### Layer 1 · Outer dithered field

Bayer-dithered (4×4) gradient that diagonals from deep navy through cyan to mint:

```
palette stops (RGB):
  ( 10,  18,  50)   navy floor
  ( 22,  40,  92)
  ( 30,  88, 144)
  ( 60, 152, 178)
  (120, 200, 198)   cyan/mint
  (180, 230, 200)   light mint ceiling
```

- Implement on a `<canvas>` sized 1/3 of the container in CSS pixels (`scale=3`) and rendered at `image-rendering: pixelated`.
- Per pixel: `t = 0.55 * x/w + 0.45 * y/h + 0.08*sin(0.06x + 0.04y) + 0.05*cos(0.09y)` clamped to `[0,1]`, mapped into the palette via Bayer-4 dither.
- Sprinkle ~1.5% "hot" pixels at `RGB(255, 240, 200)` for that scuffed-organic feel.
- Stays still — no animation. (`seed = 11` was used in the prototype.)
- See `source/plm-bacteria.jsx :: DitheredBg`.

### Layer 2 · Pixel-window chrome

Chunky 3-step bevel via stacked outer box-shadows on a single dark-navy fill (`#0a1628`):

```
0 0 0 2px  #e8f8fa     ← frameLight (top-left highlight)
0 0 0 4px  #7aa8b8     ← frameMid (mid)
0 0 0 6px  #1a3050     ← frameDark (deep navy edge)
0 0 0 7px  #000        ← outline
6 6 0 7px  rgba(0,0,0,0.45)   ← drop
```

The window has `display: flex; flex-direction: column` and fills the available height. See `VilmonicWindow` in `source/plm-bacteria.jsx`.

### Layer 3 · Title bar

- Background: `#0a1628`
- Foreground: `#e8f8fa`
- Padding: `5px 12px`
- Bottom border: `2px solid #1a3050` + `inset 0 -1px 0 #7aa8b8`
- Type: `IBM Plex Mono`, `12px`, `letter-spacing: 2px`, `font-weight: 700`
- Left cluster: three 8×8 traffic-light squares (`#ff5577`, `#ffcc44`, `#66dd88`) followed by the title `PLM · HUNDRED LINE · v2.4`
- Right cluster (chip):
  - 8×8 green pulse dot (`#7be887`, animated `plm-pulse 1.6s ease-in-out infinite`, alternating `opacity: 1 → 0.4`) + label `LINKED`
  - **Pixel button** `⇆ SPRITE` — color `amber`. See `PixelButton` for spec.

### Layer 4 · Day banner

A two-cell row directly under the title bar, bottom-bordered with `2px solid #1a3050`.

**Left cell** — fixed width ≥ 110px, padded `12px 18px`:
- Background: `linear-gradient(135deg, #80c8c8 0%, #3a98c0 100%)`
- Inset bevel: `inset 2px 2px 0 #c8eaee, inset -2px -2px 0 #1a4060`
- Foreground: `#0a1628`
- "DAY" label, 9px, letter-spacing 2px, weight 700
- Number (e.g. `100`, zero-padded to 3), 32px, weight 900, line-height 1

**Right cell** — `flex: 1`, padded `10px 16px`, three columns separated by 22px:
- ROUTE / ADDR / SPEAKER blocks
- Each label: 9px, letter-spacing 2px, color `#7aa8b8`
- Each value: 13–14px, letter-spacing 1.5px, color `#e8f8fa` (speaker name in `#80e8e8`, JP name in white via `Noto Sans JP`)
- SPEAKER is right-aligned (`margin-left: auto`)

### Layer 5 · Body (the dialogue + sprite grid)

```
display: grid;
grid-template-columns: 1fr 280px;   /* sprite right */
                       /* or 280px 1fr when sprite is on left */
gap: 14px;
padding: 14px;
background: #0a1628;
flex: 1;          /* fill remaining window height */
min-height: 0;
```

**Dialogue cell** (`flex: 1`):
- Padding 22px
- Background: `linear-gradient(180deg, #102240 0%, #0a1628 100%)`
- Inset frame: `inset 0 0 0 2px #1a3050`
- Vertical flex with `gap: 18px`, `min-height: 0`

Inside, two language rows. Each row is a 32px ⇆ 1fr grid with 12px gap and `align-items: start`.

**EN tag** (left cell of the row):
- 32px wide, centered text, `padding: 4px 0`
- Background `#80e8e8`, color `#0a1628`
- Type: 11px / weight 800 / letter-spacing 2px
- Bevel: `inset 1px 1px 0 #c8f8f8, inset -1px -1px 0 #1a5060, 0 0 0 1px #000`

**EN line text** (right cell):
- 19px / line-height 1.55 / color `#fff`
- Glow: `text-shadow: 0 0 6px rgba(128,232,232,0.4)`

**JP tag** (left cell, second row):
- Same dimensions as EN tag
- Background `#f5a93a`, color `#3a1a00`
- Bevel: `inset 1px 1px 0 #ffd07a, inset -1px -1px 0 #7a4400, 0 0 0 1px #000`

**JP line text** (right cell, second row):
- 20px / line-height 1.55 / color `#fff5e0`
- Font: `Noto Sans JP`
- Glow: `text-shadow: 0 0 6px rgba(245,169,58,0.4)`
- Trailing **caret**: 10×8 cyan rectangle (`#80e8e8`), shadow `0 0 8px #80e8e8`, blinking via `plm-caret 1s steps(1) infinite` (50% on, 50% off)

**Footer (keyboard hints)** — `margin-top: auto`, flex row with 14px gap:

| Key | Action |
|---|---|
| `SPACE` (cyan-keycap) | PLAY EN |
| `⇧ SPACE` (amber-keycap) | PLAY JP |
| `S` (light-keycap) | SPRITE SIDE |
| `↑↓` (light-keycap) | NAV |

Keycap = 1px-padded inline-block with `inset 1px 1px 0 #fff, inset -1px -1px 0 #a89cb8, 0 0 0 1px <outline>`. See the `Kbd` component for the recipe.

**Sprite cell** (280px wide, full body height):

Wraps `BacteriaPanel`:
- Bevel: `0 0 0 2px #e8f8fa, 0 0 0 4px #7aa8b8, 0 0 0 6px #1a3050, 0 0 0 7px #000`
- Inner bg: `#0d0820`
- Padding 10px, vertical flex, gap 8px
- **Header strip**: SPECIMEN label on left, `SIDE=RIGHT` (or LEFT) on right. 9px, letter-spacing 1.6, color `#e8f8fa @ 0.8`.
- **Sprite area** (`flex: 1`, grid placeItems center): `BacteriaSprite size=32 scale=9` → renders 288×288 at integer scale on `<canvas>`. Replace with the real game sprite when wiring up.
- **Caption strip** at bottom: black bar with the speaker name in `#e8f8fa`, prefix `▸ `. 10px, letter-spacing 1.4.

## Bacteria Sprite (placeholder) — algorithm

Drives a 32×32 `<canvas>` at `scale=9` (288×288 visible), `image-rendering: pixelated`.

Each animation frame (`requestAnimationFrame`):

1. Build 6 "cells" arranged on a circle of radius `4 + 1.5*sin(0.6t + i)`, angle `i/6 * 2π + 0.18t`. Each has size `s = 3 + 0.6*sin(0.9t + 1.7i)`.
2. For every pixel `(x,y)`: `field = Σ exp(-((x+0.5-cx)² + (y+0.5-cy)²) / s²)`.
3. `v = field + (bayer4(x,y)/16 - 0.5) * 0.18`.
4. Bin `v` into 5 visible bands + transparent halo:
   - `v > 0.95` → core `RGBA(255, 250, 230, 255)`
   - `v > 0.65` → hi   `RGBA(180, 240, 230, 255)`
   - `v > 0.40` → mid  `RGBA( 80, 200, 220, 255)`
   - `v > 0.22` → outer `RGBA( 30, 130, 150, 255)`
   - `v > 0.10` → halo  `RGBA( 22,  60, 100, 180)`
   - else → transparent
5. Filter the canvas with `drop-shadow(0 0 10px rgba(255,180,220,.45))`.

Reference impl: `BacteriaSprite` in `source/plm-bacteria.jsx`.

## Interactions & Behavior

| Trigger | Effect |
|---|---|
| Click `⇆ SPRITE` chip / press `S` | Toggle sprite column between right (default) and left. Persist between sessions. |
| Press `SPACE` | Replay current EN audio. |
| Press `SHIFT + SPACE` | Replay current JP audio. |
| Press `↑` / `↓` | Step to previous / next line in the dialogue. (Behavior of the underlying connector, not this UI.) |
| Game memory tick | Receive `{day, route, addr, speaker, en, jp, spriteId}`; re-render. EN/JP text fades or instant-swaps — designer's call; the prototype hard-swaps. The blinking caret only renders on the JP line (it represents "currently being read"). |
| LINKED dot | Pulses while the connector is healthy. When disconnected: dot becomes `#ff5577`, label becomes `OFFLINE`. (Not shown in prototype but implied.) |

No transitions or animation on column-flip — it's a hard cut, intentional.

## State Management

Local UI state needed:

```ts
type PLMState = {
  spriteSide: 'left' | 'right';   // user pref, persisted
  connected: boolean;             // from connector
  scene: {
    day: number;                  // 1..N
    route: string;                // 'Reset Route', etc.
    addr: string;                 // 'm020:010:030'
    speaker: { en: string, jp: string };
    line:    { en: string, jp: string };
    spriteId: string | null;      // game-side identifier
  };
};
```

The dropped scene-context drawer from earlier iterations is **not** part of V2; do not implement it.

## Design Tokens

```css
/* Outer field (V2 dithered bg palette, RGB) */
--bg-floor:   rgb( 10,  18,  50);
--bg-1:       rgb( 22,  40,  92);
--bg-2:       rgb( 30,  88, 144);
--bg-3:       rgb( 60, 152, 178);
--bg-4:       rgb(120, 200, 198);
--bg-ceiling: rgb(180, 230, 200);

/* Window chrome */
--frame-light: #e8f8fa;
--frame-mid:   #7aa8b8;
--frame-dark:  #1a3050;
--outline:     #000000;
--inner-bg:    #0a1628;
--dialogue-bg: linear-gradient(180deg, #102240 0%, #0a1628 100%);
--sprite-bg:   #0d0820;

/* Foreground */
--text-hi:     #ffffff;
--text:        #e8f8fa;
--text-dim:    #7aa8b8;
--accent-en:   #80e8e8;   /* cyan */
--accent-jp:   #f5a93a;   /* amber */

/* Status */
--status-ok:   #7be887;
--status-err:  #ff5577;
--status-warn: #ffcc44;

/* Type */
--font-mono: 'IBM Plex Mono', ui-monospace, monospace;
--font-jp:   'Noto Sans JP', sans-serif;

/* Animation */
--anim-caret: plm-caret 1s steps(1) infinite;     /* 0–50% on, 51–100% off */
--anim-pulse: plm-pulse 1.6s ease-in-out infinite; /* opacity 1 ⇆ 0.4 */

/* Spacing scale (px) */
--sp-1: 4;   --sp-2: 8;   --sp-3: 12;   --sp-4: 14;
--sp-5: 18;  --sp-6: 22;  --sp-7: 28;
```

## Assets

- **Fonts**: IBM Plex Mono and Noto Sans JP — both Google Fonts, MIT/OFL. Self-host or load via `https://fonts.googleapis.com/css2?family=IBM+Plex+Mono:wght@400;500;600;700&family=Noto+Sans+JP:wght@400;500;700&display=swap`.
- **Sprite art**: To be provided by the connector at runtime — reads from game memory. The bacteria placeholder in this handoff is generated procedurally on a canvas; no static asset.
- **Icons**: None. The traffic-light squares, status dots, and bevel edges are all CSS or canvas — no image files.

## Files

```
design_handoff_plm_redesign/
├── README.md                  ← this file
├── v2-preview.html            ← standalone V2-only mock (open in browser)
└── source/
    ├── index.html             ← multi-variation canvas (V1/V2/V3)
    ├── main.jsx               ← canvas mount
    ├── design-canvas.jsx      ← pan/zoom canvas wrapper (review tool)
    ├── plm-content.jsx        ← sample dialogue & scene data
    ├── plm-bacteria.jsx       ← DitheredBg, BacteriaSprite, VilmonicWindow, PixelButton
    └── plm-variations.jsx     ← PLM_V1, PLM_V2 (the chosen one), PLM_V3
```

To run the standalone preview locally:

```bash
cd design_handoff_plm_redesign
python3 -m http.server 8080
# open http://localhost:8080/v2-preview.html
```

(The Babel CDN scripts need real HTTP, not `file://`.)

## Open Questions for Implementation

1. **Sprite injection**: how does the connector resolve `spriteId` → an actual image? Is the source a sprite sheet (bounds + frame index) or pre-cropped PNGs?
2. **Audio replay**: same question — file path, in-memory blob, or a connector RPC?
3. **Multi-line dialogue**: a few lines in the source data are long enough to wrap to 3 lines at this width. The window has `overflow: hidden` on the outer; the dialogue cell will absorb growth via flex. If lines can grow taller than the cell can absorb, decide whether the sprite cell shrinks or the EN/JP rows scroll.
4. **Sprite-side persistence**: where? `localStorage` or the broader browser's pref store?
