Skip to content

[issues-24] sub-agent 시각 구분 강화 — glow ring, gear badge, 점선 연결선#3

Open
dadadamarine wants to merge 16 commits into
FulAppiOS:mainfrom
dadadamarine:issues-24
Open

[issues-24] sub-agent 시각 구분 강화 — glow ring, gear badge, 점선 연결선#3
dadadamarine wants to merge 16 commits into
FulAppiOS:mainfrom
dadadamarine:issues-24

Conversation

@dadadamarine
Copy link
Copy Markdown

변경 요약

마을 화면에서 sub-agent를 한눈에 구분할 수 있도록 세 가지 시각 구분 요소를 추가합니다.

추가된 시각 요소

1. Pulsing glow ring (HeroSprite)

  • sub-agent sprite 주변에 보라색(#9b72cf) 원형 outline을 렌더링
  • 1.2초 주기로 alpha가 0.35 ↔ 0.8 사이를 부드럽게 pulse해 복잡한 장면에서도 눈에 띔
  • 스케일 변화에 자동 적응 (displayWidth 기반으로 반지름 계산)

2. ⚙ gear icon badge (HeroSprite)

  • sprite 우상단에 작은 ⚙ 기호를 보라빛(#c8a8ff) 텍스트로 표시
  • 반투명 다크 배경으로 어떤 지형 위에서도 가독성 확보
  • 커스텀 텍스처 없이 유니코드 문자 재사용 → 에셋 추가 불필요

3. 점선 connector line (VillageScene)

  • 매 프레임 parent hero → attached sub-agent 사이를 CONNECTOR_DASH_LENGTH(6px) / CONNECTOR_GAP_LENGTH(4px) 점선으로 연결
  • 단일 reusable Graphics 레이어 () 를 clear 후 재사용 → GC 압력 없음
  • sub-agent가 detach되면 다음 프레임에 자동 소거

쇼츠 PiP 가시성

세 요소 모두 Phaser 캔버스 레이어에 렌더링되므로 PiP 화면에서도 동일하게 표시됩니다.

신규 파일

client/src/game/scenes/subagent-connector.ts
연결선 geometry 계산 pure 모듈 (buildConnectorSegments + visual 상수). Phaser 의존 없음 → 단위 테스트 가능.

client/src/game/scenes/subagent-connector.test.ts
11개 단위 테스트 — 선분 geometry, 상수 범위 유효성, 결정론적 출력 검증.

테스트 결과

bun test: 305 pass, 0 fail (25 files)
TypeScript: 0 errors

주의 (이슈 #23 동시 진행)

이슈 #23 (building 배치 재배치)과 동시 진행 중이나, building-layout.ts는 건드리지 않았습니다.
main sync 후 conflict 없음 확인됨.

Closes #24


🤖 Claude Code session
  • Session ID: 8669ee13-9c50-4a09-be42-4947fafa7989
  • Resume: claudeauto --resume 8669ee13-9c50-4a09-be42-4947fafa7989
  • Transcript: /Users/ms/.claude/projects/-Users-ms-Desktop-projects-worktrees-Agent-Quest-issues-20/8669ee13-9c50-4a09-be42-4947fafa7989.jsonl

dadadamarine and others added 16 commits May 19, 2026 17:56
…gle (#4)

* feat(ui): hide completed agents from village by default, add TopBar toggle (#1)

Server providers now auto-discover every Claude/Codex session jsonl on
disk, so the village would slowly fill with old completed heroes that
hadn't been touched in days. Default behavior is now to keep the village
focused on agents that are still doing something; a TopBar toggle reveals
completed heroes again for users who want the historical view.

Single source of presentation policy lives in `agentPresentation.ts` —
both the Party Bar and the Phaser scene consume the same derived list,
and source-badge mixed-mode is computed by a shared helper so React and
Phaser can't drift. TopBar stats / DetailPanel lookup / Activity Feed
keep working off the unfiltered snapshot.

Also covers the surrounding consistency work that fell out of the
review: BuildingInfoPanel now consumes the same projection, the
selection auto-clears when the toggle hides the currently-selected
hero, and the existing night/rain toggles pick up `type="button"` +
`aria-pressed` + `aria-label` to match the new toggle's a11y.

* refactor(ui): keep waiting heroes visible + move presentation policy out of hooks/ (#1)

Two issues surfaced by Codex review of the initial PR:

- waiting was getting filtered out as a side effect of the new projection.
  The Phaser scene used to render waiting heroes (HeroSprite has a dedicated
  pulse/label), and computeShowSourceBadge still counts waiting as live, so
  the scene was losing turn-end heroes and the source badge could disagree
  with what was actually on screen. Treat waiting like active/idle in the
  projection and add a regression test.

- agentPresentation.ts lived under hooks/, which made VillageScene (a Phaser
  adapter) import from the React hooks tree. The helper itself is the right
  abstraction — move it to client/src/presentation/ so both App.tsx and
  VillageScene can depend on it without crossing into React-specific land.
…ve hero sprites (#5)

* feat(agents): mirror Claude Code session display title above hero sprites

Server-side: SessionRegistry now reads the `jobId` out of every
`<configDir>/sessions/<pid>.json` it scans and resolves the matching
`<configDir>/jobs/<jobId>/state.json` for the user-set session title
(the same string Claude Code's `claude agents` view renders in the
left column). The title is exposed through a new SessionDisplayNameOracle
that AgentStateManager consults on createAgent / processEvent /
refreshAll, overriding the slug/cwd fallback whenever a display name is
present. Codex sessions and subagents (no jobId of their own) keep their
existing labels.

Client-side: HeroSprite gains updateName(name) for live retitle support,
and every head-stack label now flows through a code-point-safe
truncateLabel helper so long titles ellipsize cleanly without breaking
emoji surrogate pairs. VillageScene's hero update loop calls updateName
each tick (idempotent guard prevents redundant DOM writes), and
EditorTopBar's slot-name truncation switches to the same helper for
consistency.

Closes #2

* refactor: address Codex senior + architect review comments

- Restore the derived (slug/cwd) name when the display-name oracle
  drops the user-set title. Without the fallback path a removed or
  corrupt `state.json` would freeze the previous label on the sprite
  forever. AgentState now carries `derivedName` so the manager can
  swap back without consulting the JSONL slug again.
- Move `truncateLabel` from `client/src/game/entities/` to
  `client/src/utils/` — the helper is consumed by both the village
  game scene and the editor topbar, so its home belongs in the neutral
  utils layer instead of crossing the editor → game-entities boundary.
- Tighten the `SessionDisplayNameOracle` interface contract so it no
  longer leaks the concrete Claude storage paths into the high-level
  state policy; adapter-specific details live in `SessionRegistry`.
- Add regression tests for the oracle-drop fallback (both refreshAll
  and processEvent paths).
… (#6)

Server propagates parent ↔ child session relationship as domain data so
the client no longer infers sub-agent status from sessionId patterns.
Sub-agent heroes render at 0.7× scale and follow their parent hero with
a deterministic fan-out offset; orphaned sub-agents fall back to regular
slot-based layout.

Server
- AgentState.isSubagent + parentSessionId carry sub-agent metadata.
- file-watcher.ts treats `<parentSessionId>/subagents/` as an
  anti-corruption boundary: disk layout becomes a SubagentContext domain
  value handed up the provider chain.
- ClaudeProvider, ProviderHandlers, AgentStateManager.processEvent and
  index.ts forward SubagentContext end to end.

Client
- AgentState mirror gains the two fields; normalizeAgentState() shim
  tolerates older payloads.
- HeroSprite uses computeSpriteScale(base, isSubagent); setHeroScale()
  preserves the sub-agent factor on runtime resize.
- VillageScene tracks parentChildren / subagentParents separately from
  buildingSlots. Attached sub-agents bypass slot layout and follow the
  parent each frame via computeChildOffset() + teleportTo().

Tests
- agent-state-manager.test.ts covers main / subagent / orphan / update
  paths.
- hero-scale.test.ts pins the SUBAGENT_SCALE_FACTOR contract.
- subagent-layout.test.ts pins fan-out determinism and stable
  childIndexOf() ordering.

Closes #3
* fix(server): bound AgentState history and surface WS diagnostics (#7)

After a few weeks of Claude/Codex sessions piling up on disk, the
initial snapshot the server sends on WebSocket connect grew past 2.5 MB.
The browser would then drop the WS immediately on connect, BootScene
never received ws:connected, and the React overlay (Top Bar, Party Bar,
new completed-agents toggle) never rendered.

The runaway was `toolCalls` and `filesModified` on each AgentState
growing without bound — provider replays the entire JSONL history on
startup, and each Codex toolCall can carry several KB of aggregated
output. A small helper now keeps each agent to the most recent 50
entries on both the create and the update path.

Snapshot now also logs its byte size on every send, and any broadcast
frame that crosses 500 KB warns — so the next time the payload grows
the operator sees it before the browser does. The WS onclose handler
on both sides now records code/reason/wasClean too, because the
original symptom hunt was blocked on not knowing whether the browser
closed for 1009 (too big), 1006 (abnormal), or something else.

* fix(client): guard WS reconnect against StrictMode unmount race (#7)

Codex senior review found this is almost certainly the real driver of the
"connect → error → disconnect" loop, not the snapshot payload itself.
`onclose` was arming a reconnect timer on every close — including the
intentional close from the effect's cleanup path. On a React StrictMode
mount→unmount→mount cycle (or Fast Refresh), the orphaned hook instance's
timer would still fire, build a new WebSocket, and emit bridge events on
behalf of a hook that no longer existed, racing the freshly-mounted one.

A `shouldReconnect` ref tracks lifecycle state. `onclose` skips the
reconnect when cleanup flipped the flag, and the cleanup path now also
nulls out `wsRef.current` and the timer ref so a stale socket cannot
drive the next reconnect.

Also tightens the `MAX_*` comment in agent-state-manager — transport
concerns (byte budget, frame size warn) belong to WebSocketServer; the
state manager's cap is the retention policy of the AgentState model
itself, not a workaround for a particular transport.
* fix(client): stale-ws guard for onclose during StrictMode race (#9)

PR #8's shouldReconnect ref guards the cleanup path but not the
StrictMode mount→unmount→mount race: by the time ws_A's async onclose
fires, mount B has already flipped the same ref back to true, so the
stale ws's reconnect path runs against the freshly-mounted hook.

Identity check on wsRef.current is stricter — if the firing socket
isn't the current one, it must be a leftover from a previous mount and
must not drive a reconnect. Verified in dev: the new "closed (stale
ws)" log line fires on the unmount→remount cycle as expected.

Note: this fixes one strand of the issue #7 disconnect loop. A separate
path remains where onerror fires before any message is received and
reconnect runs on the still-current ws — that's tracked separately
because the trigger is on the server/network side, not the React
lifecycle.

* test(client): regression test for stale-ws onclose classifier (#9)

Codex senior MEDIUM: the StrictMode race fix in this PR only lives as a
guard inside an async onclose handler — if the next refactor restores
the old "reconnect on every close" behavior, no test would catch it.

Extract the three-way decision (stale / cleanup / reconnect) into a
pure `classifyCloseEvent` helper exported from useAgentState. The
onclose handler now switches on the verdict, and the helper carries
five unit tests covering the exact StrictMode mount/unmount/mount
sequence (firing-ws-not-current returns "stale" regardless of the
shouldReconnect flag).

No behavioral change — the same branches still produce the same logs
and same reconnect schedule. The refactor exists for testability.
…ace (#12)

* diagnostic(ws): tighten onerror cleanup + log Bun send result (#11)

The 1006 reconnect loop in dev didn't go away with PR #8 cap or PR #10
stale-ws guard alone — the next strand needs more data from the server
side, not just the browser side.

Two small changes:

* Drop the explicit `ws.close()` inside `onerror`. The WebSocket spec
  guarantees `onclose` fires after `onerror`, so the explicit close
  only adds a teardown race against the just-arriving server frame.
  This is a spec-correctness cleanup; it does not by itself end the
  loop in our environment.

* Log readyState before and after every `sendSnapshot`, plus the
  return value of `ws.send`. The browser already reports
  code/reason/wasClean on close; pairing that with the server's view
  of the socket at send time should triangulate whether Bun thinks
  the socket was already closing when we tried to push 160KB of
  snapshot at it.

Root cause for the loop itself is still open — this PR just makes the
next debugging session cheaper by closing the data gap.

* diagnostic(ws): add clientId to open/close/snapshot logs (#11)

Codex senior MEDIUM: under StrictMode double-mount or multi-tab usage,
the bare `[WS] snapshot bytes=…` log can't be tied back to the matching
`client disconnected` line. The server already carries `ws.data.id`
from the HTTP upgrade — include it in all three lifecycle log lines so
follow-up root-cause work on the 1006 loop has a stable event key.

Also expands the `sendResult` comment to make the Bun-specific values
explicit (-1 backpressure, 0 dropped, 1+ bytes sent) — that's the
mapping the next debugging session needs to read the logs against.
…rallelDownloads (#14)

* diagnostic(boot): log preload progress + file completes for issue #13

The BootScene → VillageScene transition is silently stalling: progress
hits ~30% and then no further filecomplete or 'complete' event fires,
so create() never runs, the ws:connected listener is never registered,
and the React overlay stays hidden forever.

Three listeners on `this.load` make the next debugging session much
cheaper:

* `loaderror` — logs which file 404'd if the issue turns out to be a
  silent 404 we never registered for.
* `complete` — confirms when (or whether) the loader actually thinks
  it's done. Today: never fires in the broken environment.
* `filecomplete` — names the last file the loader managed to finish
  before the hang. That filename is the wedge for narrowing the bug.

The progress handler also reports `totalToLoad` / `totalComplete` so we
can tell if the stall is a download or a decode step.

Empirically tried `this.load.maxParallelDownloads = 6` and it made the
stall worse — progress no longer leaves 0%, instead of getting to 30%
— so that knob is NOT the fix. This PR ships diagnostics only.

* diagnostic(boot): use Phaser constants + log totals on every event (#13)

Codex senior MEDIUM on PR #14: the diagnostic listeners need three
small reinforcements before they're actually useful for hunting the
30%-stuck root cause.

* Switch literal event names ('complete', 'filecomplete', 'progress')
  to the Phaser.Loader.Events constants. Same effect, but it now
  surfaces if a future Phaser bump renames or removes an event.
* `complete` now also logs `totalToLoad / totalComplete / totalFailed`.
  In a stuck-at-30% report, a stray `complete` event with totalFailed
  > 0 instantly tells us a silent failure path is winning.
* `filecomplete` now logs `src` alongside `key/type`. With a parallel
  loader, the last file to finish is NOT the stuck one; the stuck file
  is whatever's still in flight. Knowing the path of the last finisher
  narrows which batch the stall starts in.
* `loaderror` now also logs `key` and `type`, not just `src`.
* `progress` fires on either a pct-bucket change OR a totalComplete
  tick — so a partial-load stall inside a single bucket (queue=60,
  complete frozen at 18) doesn't get swallowed by the 10% throttle.

Still diagnostics-only. Root cause for the loader stall is the next
strand.
## Root cause (closes #15)

BootScene preload hung at ~31/83 files because React StrictMode mounts
PhaserGame twice (mount → unmount → mount). The naive useRef-per-component
pattern let cleanup call `game.destroy(true)` before the second mount
created a fresh instance, but Phaser's LoaderPlugin keeps in-flight HTTP
requests alive past destroy(). Two Phaser.Game instances then fetched the
same ~80-file asset batch in parallel, saturated the browser's HTTP/1.1
connection pool, and stalled preload — `villageReady` never fired, the
overlay never mounted, only a black canvas was visible.

Verified against baseline (a16e2fb), main (a231ac9), and v0.0.1 (67d92b9):
all three reproduced the same hang, ruling out PRs #4 / #8 / #10 / #12 /
#14 as the cause and confirming the bug is upstream-inherent.

## Fix

`PhaserGame.tsx` anchors the game in a module-level slot. The second
StrictMode mount reuses the existing instance and reparents its canvas;
cleanup defers destruction (`setTimeout(0)`) so a fast remount can cancel
it. The game tears down only on genuine unmount (page navigation).

## Related cleanup

- `text.ts` — `addCrispText` now applies NEAREST (was LINEAR, which
  bilinearly smeared text against the pixel-art tiles). Removed an
  ill-fated `setRoundPixels(true)` call — Phaser 4 dropped the per-Text
  method; sub-pixel snap belongs at game-level `roundPixels: true`.
- Fonts — Cinzel for headings (building names, Village Gate, PartyBar
  title) + Fira Code for in-canvas labels (Activity Feed and on-sprite
  labels share one typography stack). VT323 / Pixelify Sans were tried
  and rejected: pixel-style faces blur at 10–11px even with NEAREST.
- Label readability — replaced stroke + drop shadow with a translucent
  black plate (`rgba(0,0,0,0.65)` + 4×2 padding) on the activity label,
  matching the PartyBar pattern.

## Label IA — partial (continues in #16)

Started consolidating on-sprite text: only the activity label remains in
HeroSprite. Name / subagent / source badge / model / detail / task all
move to PartyBar + Detail Panel per the new IA. This commit still leaves
broken references in `teleportTo` / `setSelected` / `updateDepth` /
`setName` / `layoutSubagentAndSource` / `layoutActivityAndModel` /
`setModelBadge` — those, plus the new "Name under feet (2 lines) + speech
bubble (task + live activity message) above head + PartyBar index
numbers" UI, are tracked in #16.

Refs: closes #15, follows up #16
* feat: hero label IA — bubble above head + name under feet + party index

## Information architecture (closes #16)

The on-sprite label stack was 7 labels deep (name / subagent / source /
activity / model / detail / task), all stacked above the head, all
duplicating data the PartyBar already shows. With 5–10 heroes on screen
the canvas was illegible.

New layout:

  ┌─────────────┐
  │ [N]         │  ← indexText (top-right, matches PartyBar row number)
  │             │
  │ task...     │  ← taskText (user prompt)
  │ msg...      │  ← activityMsgText (last Activity Feed message)
  │  ▼          │
  │ [SPRITE]    │
  │             │
  │ hero name   │  ← nameText (2 lines, color reflects activity/status)
  │ ...wrapped  │
  └─────────────┘

Subagent / source / model / activity-word are PartyBar's job now.
`setSourceBadgeVisible` and `setModel` are no-op stubs kept so existing
callers stay compatible.

## Party-bar match

PartyBar applies `STATUS_ORDER` (active < waiting < idle < error <
completed) when rendering. VillageScene mirrors the same sort and
assigns 1-based indices via `hero.setIndex(i+1)` on every agent-state
tick — so the number above each sprite is the same number on the panel
row. The marker is rendered with a small dark plate (`rgba(0,0,0,0.8)`)
at the sprite's top-right and as an absolutely-positioned chip on the
PartyBar avatar's top-left.

## File-level changes

- `HeroSprite.ts` — full rewrite around the new label fields. The
  previous broken state (name/detail/task/subagent/source/model fields
  referenced by 8+ methods after the creation calls were removed) is
  cleaned up. Activity / waiting / error states now flow into
  `nameText.setColor` + alpha-pulse, replacing the separate activity
  label.
- `VillageScene.ts` — new `STATUS_ORDER` constant + index assignment
  pass at the end of the agent-update loop.
- `PartyBar.tsx` + `PartyBar.css` — `PartyRow` accepts `index`; renders
  a `.partybar-index` chip on the avatar.

## Follow-up

- Speech-bubble tail graphic (sprite-direction arrow) — listed as
  optional in the issue, deferred.
- Activity feed message source binding currently reuses
  `currentCommand` / `currentFile` (already per-hero). If the real
  Activity Feed event stream needs to feed in, that can ride a
  follow-up after the IA is validated on the user's side.

Closes #16

* refactor(hero): tighten bubble + move index next to name

- Above-head bubble sits ~2 px from the sprite (was ~16 px). The earlier
  gap made the message read as floating; pulling it in makes the bubble
  feel like direct speech.
- Index marker moves from the sprite's top-right to the left of the name
  label on the same row below the feet. Origin (1, 0) anchors its right
  edge so it always butts against the sprite's left edge regardless of
  the name's wrapped width. Reads as "[N] hero-name" and lines up with
  the PartyBar row's leading index.

* feat(hero): draw a real speech bubble (rounded plate + tail) for the head message

Previously the task / activity-msg lines lived in two separate Text
objects, each with its own translucent backgroundColor — they read as
"two stacked chips," not as one bubble. The new design pulls the plate
out into a Graphics object sized in `updateBubble()` to wrap both lines
(or just one when only one is present) and adds a small downward tail
pointing at the sprite's head, so the message reads as direct speech.

- New `bubbleBg: Graphics` field. The two Text objects keep their
  positions and colors but no longer carry `backgroundColor` themselves.
- `updateBubble()` recomputes width/height from `displayWidth` of the
  visible lines, draws a rounded rect with a faint title-line stroke,
  and a triangular tail. Hides the plate when both lines are empty.
- Wired into `updateTask` / `updateDetail` (text changes) and
  `teleportTo` / move-tween onUpdate (position changes).

* fix(hero): align bubble plate to actual text bounds + tighten name gap

- Bubble plate now derives its bounds from actual text.y and
  text.displayHeight (origin 0.5, 1 → bottom = text.y, top = text.y -
  displayHeight). The previous hardcoded lineH × lineCount formula
  drifted from the real text size and produced a misaligned plate.
- Tail increased to 8×6 px (was 6×4) for visibility.
- Name offset reduced from halfH+6 to halfH+2 — sits right under
  the sprite's feet instead of floating below.

* fix(hero): shorten label truncation + raise label depth above buildings

- Truncation caps: NAME 32→24, TASK 38→22, ACTIVITY_MSG 40→22 chars.
  Long URLs and paths now cut early so the bubble stays inside the
  sprite's visual footprint.
- Label depth raised from footY+0.6 to footY+1.0–1.2 so hero labels
  render in front of building names (buildings sort by their own foot-y
  and topped out at footY+0.6).

* fix(hero): pull name label flush with sprite feet (halfH-2)

* fix(hero): name at exact foot line (halfH, 0 gap)

* fix(hero): compensate sprite frame padding — name at visual foot line

* fix(hero): name tighter to feet (halfH-14)

* fix(hero): name even tighter (halfH-20)

* fix(hero): name halfH-24

* fix(hero): single-line name (18 chars, no wordWrap)

* fix(hero): bubble closer to head (halfH-10)

* fix(hero): widen gap between index marker and name label

* feat(hero): parchment speech bubble + index at top-left

- Speech bubble restyled by design-director: parchment cream background
  (0xF5E0B0 α0.92) + dark brown border (0x8B5E3C 2px) + larger tail
  (10×8) with border edges. Text colors flipped to dark brown for
  contrast on the light plate. Matches the Tiny Swords warm-tone palette.
- Index marker moved from name-row (bottom) to sprite's top-left corner
  — sits snug at the head, visually distinct from the name label below.

* feat(hero): ghost unselected bubbles (α 0.4), full opacity on select

* fix(hero): keep bubble at full opacity regardless of selection

* fix(hero): pull index marker closer to sprite body

* fix(hero): index at exact sprite top-left corner (-halfW, -halfH)

* fix(hero): index 12px inward from top-left corner

* fix(hero): index further right (halfW-18)

* fix(hero): index further down (halfH-18)

* fix(hero): index nudge down+right (halfW-22, halfH-22)

* fix(hero): bubble lower (halfH-16)

* fix(hero): index further down+right (halfW-26, halfH-26)

* fix(hero): index nudge (29)

* fix(hero): index right (33)

* fix(hero): index down (33)

* fix(hero): index right (36)

* fix(hero): index right (38)

* fix(hero): index up (30)

* feat(feed): prefix agent name with party index in Activity Feed

* fix(hero): index up tiny (28)
* fix(hero): bubble closer to head (halfH-24)

* feat(hero): allow 2-line wrap in bubble (44 chars + wordWrap 140px)

* feat(hero): 3-way visual distinction in bubble (size + weight + color)

* fix(text): restore LINEAR filter for canvas text (match Activity Feed quality)
1. Idle wander — heroes in idle state now randomly walk within ±12px
   of their grid slot every 3-7 seconds, keeping the village alive
   even when no work is happening. Stops when activity changes.

2. Completion VFX — gold/orange/yellow particle burst (12 particles,
   800ms) fires when a hero's status changes to 'completed'. Uses the
   existing 'px' texture + explode() pattern from TerrainRenderer.

3. Brightness boost — additive white overlay (0xFFFFFF α0.06) on the
   world rect lifts dark forest tones so the village reads clearly as
   a PiP element in YouTube Shorts.

Closes #20
* feat: idle wander + completion VFX + brightness boost (#20)

1. Idle wander — heroes in idle state now randomly walk within ±12px
   of their grid slot every 3-7 seconds, keeping the village alive
   even when no work is happening. Stops when activity changes.

2. Completion VFX — gold/orange/yellow particle burst (12 particles,
   800ms) fires when a hero's status changes to 'completed'. Uses the
   existing 'px' texture + explode() pattern from TerrainRenderer.

3. Brightness boost — additive white overlay (0xFFFFFF α0.06) on the
   world rect lifts dark forest tones so the village reads clearly as
   a PiP element in YouTube Shorts.

Closes #20

* feat(building): replace building name with activity label

* fix(building): larger activity label (17px)

* fix(building): Fira Code + backdrop plate, remove stroke/shadow noise

* fix(text): lower resolution to DPR (remove 2.5x oversampling noise)
…vement (#23) (#25)

- Library (reading) near south gate — first stop after spawn
- Castle (thinking) above library — natural reading → thinking flow
- Forge/Arena/Alchemist form tight central cluster (106-180px apart)
  replacing the old editing↔bash gap of 550px across the village
- Chapel (git) and Watchtower (reviewing) share NE corner
- Tavern (idle) anchors the western plaza
- All buildings remain inside CITY_CLEAR ellipse (max factor 0.87)
- Update road network waypoints/edges to match new layout
- Update PLAZA anchor to center-junction (1400, 780)
- Sync server/src/map/protected-buildings.ts default coordinates
- Add forge-access (11) and arena-access (12) waypoints to road network
  so BFS routes heroes to actual building coordinates, not nearby junctions
- Add editing/bash/debug triangle edges (11↔12, 11↔8, 12↔8)
- Update road comment to WHY-focused (workflow decision rationale)
- Update building-layout comment to WHY-focused
- Add Readonly<Point> return type to getRoadSegments() to prevent
  accidental mutation of internal waypoint objects
- Add protected-buildings.test.ts to catch client/server coord drift
…nnector line (#24)

- HeroSprite: add pulsing glow ring (purple outline) + gear icon badge (⚙)
  for sub-agent sprites at construction time; update positions in teleportTo()
  and moveTween onUpdate; clean up in destroy()
- VillageScene: draw dashed connector lines from parent hero to each attached
  sub-agent every frame using a reusable Graphics layer; clear on detach
- subagent-connector.ts: pure module (buildConnectorSegments + visual constants)
  so geometry is unit-testable without Phaser
- subagent-connector.test.ts: 11 passing unit tests covering segment geometry,
  constants validity, and determinism
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