Skip to content

feat(web): Lisa Room — ambient, state-driven pixel-art living space#214

Merged
oratis merged 2 commits into
mainfrom
claude/amazing-grothendieck-7e667e
Jul 12, 2026
Merged

feat(web): Lisa Room — ambient, state-driven pixel-art living space#214
oratis merged 2 commits into
mainfrom
claude/amazing-grothendieck-7e667e

Conversation

@oratis

@oratis oratis commented Jul 12, 2026

Copy link
Copy Markdown
Owner

What

A new Room — an immersive pixel-art scene where the full-body Lisa character "lives", reachable from the GUI sidebar (⌂ Room) and at GET /room. Unlike a scripted companion wallpaper (N0va / BSide), every layer is driven by Lisa's real state — a read-only projection of what she's actually doing.

N0va is "a house with no one home"; Lisa was "someone with no house." This gives her one — and every light in it reflects her real state.

Character — full-body animatable spritesheet (not a bust)

The earlier version composited her half-body mood portrait into the room (looked like a floating head). Replaced with a full-body sprite standing on the floor:

  • lisa-idle.png — 2-frame sheet (eyes open | closed): breathing via CSS transform, blink flips the frame on a JS timer.
  • lisa-sit.png / lisa-sleep.png — pose sprites, swapped by mood/state: working/reading → sits with a glowing laptop; napping / Reve dreaming → curled asleep.
  • Generated via an anchor → keyframes → chroma-key + foot-anchor pipeline (cf. Ludo.ai / chongdashu/ai-game-spritesheets) using gemini-2.5-flash-image on Vertex, keyed + normalized with PIL.

Scene

  • room-{day,dusk,night}.png — one pixel room, three time-of-day variants (image-to-image kept the furniture identical), crossfaded by the local clock.
  • Layered DOM/CSS 2.5D diorama — no canvas / WebGL / bundler, mirrors src/web/island.ts. Blurred backdrop fill, contained square stage foot-anchored to the floor, mood lighting, weather + firefly particles, a "thinking" monitor glow, dreaming dim + floating Z's, and a desk letter that reads her ★ while-you-were-away note.

Wiring (no new backend state, no new deps)

  • GET /room serves a self-contained page.
  • Subscribes to /events (mood / chat_start·end / idle_*) + polls /api/island/ping (mood, current_desire, unread) — shared with the Island.
  • GUI: ⌂ Room nav tab + #viewRoom iframe, lazily loaded on first open.
  • Does not touch soul / mood / heartbeat / Reve — read-only.

Verified end-to-end

On a real lisa serve --web (isolated LISA_HOME, spare port): /room route, /api/island/ping (real soul desire), SSE /events, the GUI ⌂ Room tab → iframe → /room, full-body render, real-clock dusk, zero console errors. State transitions (sit / sleep / thinking / letter / blink / day-night) verified via faithful SSE replay.

Design + build notes: docs/PLAN_ROOM_v1.0.md.

Files

  • new: src/web/room.ts, src/web/assets/room/ (3 room bgs + 3 character sprites), docs/PLAN_ROOM_v1.0.md
  • changed: src/web/server.ts (route), src/web/lisa-html.ts + src/web/lisa-client.ts (nav tab + iframe)

Not in this PR / next

Manual outfit + room-theme switching · current_desire as an in-room prop · you→her desk note · standalone wallpaper window (Swift, like the Island) · more emotion poses · aligning the sprite pipeline with Luddi's actual Seedance frame flow.

🤖 Generated with Claude Code

…/room)

A new immersive scene where the full-body Lisa character "lives", reachable
from the GUI sidebar (⌂ Room) and at GET /room. Unlike a scripted companion
wallpaper, every layer is driven by Lisa's REAL state — not a canned loop.

Character (full-body animatable spritesheet, not a bust):
- lisa-idle.png — 2-frame sheet (eyes open|closed): breathing via CSS transform,
  blink flips the frame via a JS timer.
- lisa-sit.png / lisa-sleep.png — pose sprites, swapped by mood/state
  (working/reading → sit w/ glowing laptop; napping / Reve dreaming → sleep).
- Generated via an anchor → keyframes → chroma-key + foot-anchor pipeline
  (cf. Ludo.ai / chongdashu/ai-game-spritesheets), gemini-2.5-flash-image on
  Vertex; keyed + normalized with PIL. Fixes the earlier floating-bust look.

Scene:
- room-{day,dusk,night}.png — one pixel room, three time-of-day variants
  (image-to-image kept furniture identical); crossfaded by the LOCAL clock.
- Layered DOM/CSS 2.5D diorama (no canvas/WebGL/bundler): backdrop-fill,
  contained square stage foot-anchored to the floor, mood lighting, weather +
  firefly particles, monitor "thinking" glow, dreaming dim + floating Z's,
  a desk "letter" that reads Lisa's ★ while-you-were-away note.

Wiring (reuses existing signals, adds no backend state, read-only projection):
- GET /room serves a self-contained page (mirrors src/web/island.ts).
- Subscribes to /events (mood / chat_start·end / idle_*) + polls
  /api/island/ping (mood, current_desire, unread) — shared with the Island.
- GUI: ⌂ Room nav tab + #viewRoom iframe, lazily loaded on first open.

Verified end-to-end on a real `lisa serve --web`: route, ping (real soul
desire), SSE, the GUI tab → iframe → /room, full-body render, real-clock dusk,
no console errors. State transitions (sit/sleep/thinking/letter/blink/day-night)
verified via faithful SSE replay. Design + build notes in docs/PLAN_ROOM_v1.0.md.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

@oratis oratis left a comment

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Review — Lisa Room (/room)

整体质量很高:自包含 ROOM_HTML 完全对齐 island.ts 先例;SSE 走无名 message 事件 + m.type 与服务端 broadcast() 格式一致(含 idle_error);/assets/room/* 走既有静态路由(有 .. 穿越防护,SW cache-first 懒缓存自动覆盖);iframe 懒加载 + .view.active 是 flex column,flex:1 能正常撑满;idleText/desire 均走 textContent,无注入面。只读投影承诺成立——没有触碰 soul/mood/heartbeat 的任何写路径。

发现 8 个可优化点(1 个小 bug + 2 个性能 + 2 个状态映射误判 + 2 个 UX + 1 个清理),全部见行内评论。没有 blocker。

总体:批准方向,建议全部修完再合。

🤖 Generated with Claude Code

Comment thread src/web/room.ts
state.dreaming = false; body.classList.remove('dreaming'); applyTOD(); applyPose();
state.unread = true; state.idleText = (m.text || '').slice(0, 4000);
refreshDot(); refreshCaption(); refreshLetter();
document.body.animate([{ filter: 'brightness(1.25)' }, { filter: 'brightness(1)' }], { duration: 700 });

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Bug(小)idle_messagestate.dreaming 置回 false,但没有调 refreshDesire()。dreaming 期间 desire ticker 被隐藏(refreshDesire!state.dreaming 条件),梦结束收到信之后 ticker 会一直藏着,最长要等 30s 的下一次 ping 才恢复。idle_start/idle_done 分支都调了,这里漏了。

Comment thread src/web/room.ts
var weather = $('weather');
function clearAmbient() { while (weather.firstChild) weather.removeChild(weather.firstChild); }
function refreshWeather() { rebuildAmbient(); }
function rebuildAmbient() {

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

性能setMood()refreshWeather()rebuildAmbient() 会在每一条 mood SSE 上清空并重建最多 70 个粒子 DOM 节点,即使天气模式完全没变(比如从 happy 切到 excited,两者都无天气)。建议记一个 ambient key(weatherOf(mood) + ':' + tod + ':' + dreaming),key 不变就直接 return,避免无谓的 DOM churn 和动画重启。

Comment thread src/web/room.ts
tx = (e.clientX / window.innerWidth - 0.5);
ty = (e.clientY / window.innerHeight - 0.5);
});
function raf() {

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

性能:视差 rAF 循环无条件 60fps 常驻——标签页隐藏时 rAF 会被浏览器节流但仍在跑,且每帧都写 stage.style.transform(即使指针根本没动)。这个页面的定位是常驻氛围窗,建议:① document.hidden 时跳过写入;② 增量小于阈值时不写 transform;③ 顺带尊重 prefers-reduced-motion(CSS 里已经处理了动画,但视差是 JS 驱动的,漏了)。

Comment thread src/web/room.ts
'sleepy': 'sleepy', 'shy': 'a little shy', 'surprised': 'surprised', 'neutral': 'at home',
};
// Weather-flavored moods → particle mode.
function weatherOf(slug) {

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

状态映射误判/rain|storm/ 会命中 outfit 立绘 raincoat——她只是穿了雨衣,房间里就下起雨来了 😄。建议收紧为 /rain(?!coat)|storm/ 或改为显式 slug 集合(in-rain / stormy)。

Comment thread src/web/room.ts
// ── Full-body pose. The sprite is pose-based (stand / sit / sleep); facial
// mood nuance is carried by the caption + lighting, not 114 faces. ─────────
var lisaWrap = $('lisa-wrap');
function poseFor() {

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

状态映射误判/sleep|nap/ 会命中 emotion 立绘 sleepy——她只是犯困,不该直接切到蜷睡姿势躺下(caption 还显示 'sleepy',状态栏说她困了、画面里人已经睡着,自相矛盾)。建议收紧为 /sleeping|napping/

Comment thread src/web/room.ts
$('reader-close').addEventListener('click', dismissUnread);

// ── Open the full chat (native bridge if inside a Lisa app window). ──────
function openChat() {

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

UX:Room 的主要入口是主 GUI 里的 iframe(#roomFrame)。在这个场景下点 "Talk to her" 或点 Lisa 本人会走 window.open('/', '_blank')——弹出第二个完整 GUI 标签页,而用户其实就在 GUI 里,只差切回 chat 视图。建议:嵌在 iframe 里时(window.self !== window.top)向 parent postMessage,由 lisa-client.ts 监听并 showView('chat');native bridge 和独立打开的分支保持不变。

Comment thread src/web/room.ts Outdated
<div id="offline">
<div class="z">☾</div>
<div>Lisa is asleep</div>
<div class="chip" id="chip-wake">Wake her — run <code>lisa serve --web</code></div>

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

UX(小)#chip-wake 用了 .chip 样式(cursor:pointer + hover 抬升),看起来可点,但没有绑任何 handler——而且此时服务器已经下线,页面本身也无法代为执行 lisa serve --web。建议去掉交互态(非 chip 的纯文本样式),或者点击时把命令复制到剪贴板。

Comment thread src/web/room.ts
a transparent bust; we anchor her bottom near the rug and let her breathe. */
/* Lisa — a FULL-BODY animated sprite standing on the rug (not a bust). The
idle sheet has 2 frames (eyes open | eyes closed) side by side; breathing
is a CSS transform, blinking flips the background frame. Pose swaps

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

清理:这里留了两段相邻的角色注释,上面这段还是 v1 半身像("transparent bust")时代的旧文案,和紧随其后的 full-body 新注释矛盾,删掉旧的。另外 :root 里的 --stage: min(100vh, 100vw) 定义后全文没有引用(#stage 直接写了 min(100vw,100vh)),是死变量,删掉或真正用上。

@oratis

oratis commented Jul 12, 2026

Copy link
Copy Markdown
Owner Author

CI 红灯(review 补充 #9:这个 PR 改了 MAIN_HTML(新 nav 按钮 + #viewRoom iframe + loadView 分支),但没按 src/web/lisa-html-snapshot.test.ts 的约定重算 EXPECTED_LENGTH / EXPECTED_SHA256,所以 checks 目前 fail。修复时按文件头注释里的命令重算两个常量,并在 changelog 注释里补一行 "Then: …"。

🤖 Generated with Claude Code

Review fixes for #214:
- idle_message now calls refreshDesire() so the desire ticker reappears
  as soon as dreaming ends (was hidden up to 30s until the next ping)
- rebuildAmbient() keyed on weather:tod:dreaming — mood pulses no longer
  rebuild ~70 particle nodes when the ambient mode didn't change
- parallax rAF skips work while the tab is hidden or the easing has
  settled, and stays off under prefers-reduced-motion
- weatherOf(): 'raincoat' outfit no longer summons indoor rain
- poseFor(): 'sleepy' no longer forces the sleep pose (sleeping/napping only)
- openChat() inside the GUI's #roomFrame posts room_open_chat to the
  parent (lisa-client switches to the chat view) instead of opening a
  duplicate GUI tab
- offline "Wake her" chip → non-interactive hint (it had no handler)
- dropped the stale bust-era comment + unused --stage var
- recomputed MAIN_HTML snapshot constants (nav item + #viewRoom iframe
  + message listener) — un-reds CI

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@oratis oratis merged commit 6d10eb6 into main Jul 12, 2026
1 check passed
@oratis oratis deleted the claude/amazing-grothendieck-7e667e branch July 12, 2026 12:34
oratis added a commit that referenced this pull request Jul 12, 2026
- package.json 0.14.0 → 0.15.0 (npm version; also syncs the stale
  0.13.0 in package-lock.json)
- docs/RELEASE_v0.15.0.md — headline: Lisa Room (#214); plus
  ElevenLabs-first ASR (#174), Cloud Run Anthropic relay (#209), docs
  (#212, #213)

Typecheck green, 915 tests green on this tree.

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant