← Back to Dev Blog

Daily Scrum Log — 2026-05-21 (Day 6) · market_gossip

Summary

The town learned to talk. Adjacent villagers now exchange short, profession-flavored chats during daytime hours, and the canvas floats a small speech bubble between them. The chats appear in the chronicle with a 💬 marker. Eleven new tests bring the suite to 79 passing.


Project Manager

Contribution: Sequenced today's work against the Day 6 milestone in GOALS.md and Day 5's "Tomorrow" notes.

Key Decisions:

  • Picked Villager Social Events over the other Day 6 candidates (persistence load, multi-hero, A*). Reasoning: it's the natural payoff of yesterday's 14-villager population — the visible result of social mechanics will be felt immediately, while persistence and pathfinding are invisible polish.
  • Capped today's scope: detection + flavor + bubble only. No relationship state, no familiarity counters, no friendships yet — those become tomorrow's depth feature once the surface mechanic is live.
  • Locked the cooldown at 4 hours per pair to keep the chronicle readable but lively. With ~14 villagers, that's still room for half a dozen chats per in-game day.
  • Made chats purely additive: the SocialSystem is a leaf module the SimEngine calls once per tick, and producing zero chats is the safe default. Lower risk to the existing Day 5 tick path.

Reasoning: The simulation now has named villagers walking schedules; the next thing the user will feel missing is signs of life between them. A flavor chat at the market square is the smallest atomic step toward "society."


World Designer

Contribution: Decided what kinds of conversations would feel right in a fantasy town and which profession pairings should have flavored lines.

Key Decisions:

  • Wrote a curated table of 13 profession-pair flavor templates: Farmer ↔ Merchant haggles over turnips; Farmer ↔ Smith talks plows and scythes; Priest ↔ Guard exchanges blessings; Guard ↔ NightWatch hands off the watch; NightWatch ↔ NightWatch whispers about strange sounds. Generic fallback (5 lines) covers any pair without a dedicated template.
  • Adjacency uses Chebyshev distance ≤ 1 — orthogonal and diagonal neighbours both count, plus the same-tile case. This matches the player's expectation that two villagers "next to each other" should chat, even when they meet at a corner.
  • Chats are suppressed during night so the world feels quieter when it should be quiet. Dawn, day, and dusk all permit conversation.
  • The bubble visual sits halfway between the two chatting villagers and floats one quarter-tile above them, so the eye reads the pair as the source.

Backend Developer

Contribution: Implemented SocialSystem.js and integrated it into SimEngine's tick loop.

Files Created:

  • server/SocialSystem.js — Stateless detection + a small Map cooldown table + a TTL-managed list of active bubbles. Public API: tick(villagers, hour) → chats[] and serialize() → bubbles[]. Exports isAdjacent, pairKey, PAIR_COOLDOWN_HOURS, BUBBLE_TTL for tests.

Files Modified:

  • server/SimEngine.js — Owns a SocialSystem instance, calls it after NPCManager.tick, logs each chat as a social event (💬 in the UI), and exposes the active bubbles in the snapshot as chats: [...].

Key Technical Decisions:

  • Stored cooldowns by a normalized pairKey(a, b) (smaller id first), so the same pair only counts once regardless of which iteration order finds them.
  • Pairwise comparison is O(N²) but N=14 villagers and a Chebyshev short-circuit on the X delta makes this trivially cheap — no need for spatial indexing yet.
  • Flavor selection is deterministic for a given pair ((a.id * 7 + b.id * 13) % flavors.length). The randomness comes from which pair is adjacent each tick. Deterministic picks made the profession-flavor test possible without freezing Math.random.
  • Capped at 3 chats per tick (MAX_CHATS_PER_TICK) — when villagers cluster on a shared home tile at night-start, we don't want a single tick to dump 8 messages into the chronicle.
  • Bubble TTL is in ticks (= in-game hours), not real seconds. Stays consistent if TICK_MS ever changes.

Infrastructure note: Hit the same SimEngine.js truncation issue that Day 5 flagged — the Edit tool silently chopped the last ~13 lines mid-string when applied to long files. Resolved by rewriting the full file via bash heredoc, then node --check confirmed clean parse before re-running tests. The Day 5 mitigation note holds: always node --check after a SimEngine edit.


Frontend Developer / Designer

Contribution: Wired the new chats snapshot field into the canvas and the chronicle.

Files Modified:

  • client/src/components/WorldMap.jsx — New pass between the villager dots and the night overlay: draws a small parchment-colored elliptical bubble (7×5 px) with a tiny tail pointing down at the chatting pair, plus three dark dots inside to read as "…". Drawn before the night overlay so dusk/night still dims them naturally.
  • client/src/components/EventLog.jsx — Added social and time event types with 💬 and icons in soft lavender / blue-grey. Yesterday's phase-transition events were already firing but had no icon entry; this also gives them a dedicated row treatment.
  • client/src/App.jsx — Passes chats={state?.chats ?? []} to WorldMap.

Design Decisions:

  • Bubble color is the parchment cream (#f8f0d2) used in the building labels — sits naturally on the warm tile palette without inventing a new accent.
  • Three dots, not text, on the canvas — text in a 7×5 px ellipse would be unreadable, and the dots read instantly as "they're talking." The actual chat content already shows up in the chronicle below.
  • TTL of 2 ticks = ~7 real seconds at the current TICK_MS=3500. Long enough to register, short enough not to clutter the map.

Tester / QA

Contribution: Wrote server/SocialSystem.test.js. Re-ran the full suite.

Test Coverage (11 new tests):

  • isAdjacent — corner + same-tile + far-away cases.
  • pairKey — symmetric under argument swap.
  • Day chat fires for two adjacent day-workers at noon.
  • No chats emitted at night (phase suppression).
  • Same pair cannot chat two ticks in a row (cooldown enforced).
  • Same pair can chat again after PAIR_COOLDOWN_HOURS.
  • Cluster of 8 co-located villagers triggers at most MAX_CHATS_PER_TICK chats.
  • serialize() produces bubbles with midpoint coordinates and message text.
  • Bubbles age out after TTL when no new chat refreshes them.
  • Profession-flavored line wins over generic when a Farmer–Smith pair chats (asserts "plow|scythe" in message).
  • Non-adjacent villagers produce no chats.

Results: npm test79 / 79 passing (was 68; +11 SocialSystem). Smoke test from a live SimEngine instance confirms the snapshot serializes to JSON cleanly with the new chats field, and a deterministic placement of two villagers at (11,11) and (12,11) reliably produces a social event in the chronicle.

Risk Log:

  • Math.random is no longer used in chat-line selection, but Villager.pickName still randomizes villager names at startup, so the profession-flavor test relies on profession (deterministic) rather than name (random) in its assertion.
  • File-edit truncation hit SimEngine.js again on a string ending with an em-dash; mitigated by full-file rewrite via heredoc and node --check. Long-standing risk — would be worth adding a CI/precommit node --check server/SimEngine.js rather than relying on memory.

What Was Built

  • New SocialSystem module: detection, cooldown, profession-flavored templates, active-bubble TTL
  • 14+ flavor templates across 13 profession pairings, plus generic fallback
  • social event type in the chronicle with a 💬 marker, lavender color
  • Canvas chat bubbles drawn between adjacent villagers (parchment ellipse + tail + three dots)
  • Snapshot exposes a chats: [...] array alongside villagers and time
  • 11 new tests; suite green at 79/79

Tomorrow (Day 7 Candidates)

  • Persistence load — read the latest saves/hero-*.json on server boot and resume from it. Closes the Day 3 write stub.
  • Relationship tracking — extend the cooldown map into a familiarity counter so frequent chatters unlock richer lines and a "friend" tag. Foundation for Day 8 households/families.
  • A pathfinding* — replace Manhattan stepping so villagers walk around buildings before we ever try drawing roofs.
  • Second town — multi-settlement map and a world-overview switcher.