Spaces:
Sleeping
Sleeping
Initial submission of Analog Town to Build Small Hackathon
Browse files- .gitattributes +3 -0
- SUBMISSION_BLOG.md +124 -23
- VIDEO_SCRIPT.md +167 -0
- __pycache__/app.cpython-312.pyc +0 -0
- __pycache__/theme.cpython-312.pyc +0 -0
- app.py +89 -82
- docs/2026-06-11-new-towns-image-handoff.md +166 -0
- lotus_wharf_map.png +3 -0
- sample_towns/lotus_wharf.json +156 -0
- sample_towns/tin_lantern_junction.json +156 -0
- sample_towns/verdant_spire.json +156 -0
- theme.py +281 -10
- tin_lantern_junction_map.png +3 -0
- verdant_spire_map.png +3 -0
.gitattributes
CHANGED
|
@@ -51,3 +51,6 @@ port_whisper_map.png filter=lfs diff=lfs merge=lfs -text
|
|
| 51 |
rustwood_map.png filter=lfs diff=lfs merge=lfs -text
|
| 52 |
static_ambient.wav filter=lfs diff=lfs merge=lfs -text
|
| 53 |
town_map.png filter=lfs diff=lfs merge=lfs -text
|
|
|
|
|
|
|
|
|
|
|
|
| 51 |
rustwood_map.png filter=lfs diff=lfs merge=lfs -text
|
| 52 |
static_ambient.wav filter=lfs diff=lfs merge=lfs -text
|
| 53 |
town_map.png filter=lfs diff=lfs merge=lfs -text
|
| 54 |
+
lotus_wharf_map.png filter=lfs diff=lfs merge=lfs -text
|
| 55 |
+
tin_lantern_junction_map.png filter=lfs diff=lfs merge=lfs -text
|
| 56 |
+
verdant_spire_map.png filter=lfs diff=lfs merge=lfs -text
|
SUBMISSION_BLOG.md
CHANGED
|
@@ -1,42 +1,143 @@
|
|
| 1 |
-
#
|
| 2 |
-
### A Whimsical Submission to the Hugging Face Build Small Hackathon (Whimsical Track)
|
| 3 |
|
| 4 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 5 |
|
| 6 |
---
|
| 7 |
|
| 8 |
-
##
|
|
|
|
|
|
|
|
|
|
|
|
|
| 9 |
|
| 10 |
-
|
| 11 |
|
| 12 |
-
**
|
| 13 |
|
| 14 |
-
|
|
|
|
|
|
|
| 15 |
|
| 16 |
---
|
| 17 |
|
| 18 |
-
##
|
| 19 |
|
| 20 |
-
|
| 21 |
-
- **
|
| 22 |
-
- **
|
| 23 |
-
- **Dynamic Fading Manager**: Written in client-side Javascript, our audio manager automatically monitors the receiver's signal strength. When you tune close to an agent's frequency, the static volume smoothly fades down to zero; when you scan between stations, the crackle and hiss swell back up to your volume slider's setting.
|
| 24 |
|
| 25 |
---
|
| 26 |
|
| 27 |
-
##
|
| 28 |
|
| 29 |
-
|
| 30 |
-
- **
|
| 31 |
-
- **
|
| 32 |
-
- **Interactive Pins**: Clicking on any character token dynamically updates the radio receiver to their frequency, loads their emotional dossier, and prints their latest intercepted thought stream.
|
| 33 |
|
| 34 |
---
|
| 35 |
|
| 36 |
-
##
|
|
|
|
|
|
|
|
|
|
|
|
|
| 37 |
|
| 38 |
-
|
| 39 |
-
1. **Model Parameter Constraint (<32B)**: Powered by the Hugging Face Inference API client, we use `Qwen/Qwen2.5-7B-Instruct` as our primary model, with an automatic fallback to `Qwen/Qwen2.5-14B-Instruct` if rate limits are hit.
|
| 40 |
-
2. **Deterministic State Transitions**: Rather than standard free-form generation, we model minds as discrete Pydantic structures (see schemas.py). The model outputs valid JSON conforming to:
|
| 41 |
-
$$\text{Previous State} + \text{Broadcast Event} \rightarrow \text{Updated State} + \text{Monologue} + \text{Likely Actions}$$
|
| 42 |
-
3. **Local Dataset Exports**: Traces are collected, cleaned of PII, packed, and uploaded as structured datasets to the Hugging Face Hub using export_hub.py.
|
|
|
|
| 1 |
+
# Analog Town: A Shortwave Simulator for Fictional Minds
|
|
|
|
| 2 |
|
| 3 |
+
*A submission to the Hugging Face Build Small Hackathon — Whimsical Track*
|
| 4 |
+
|
| 5 |
+
> **Live Space:** https://huggingface.co/spaces/build-small-hackathon/analog-town
|
| 6 |
+
> **Model:** `Qwen/Qwen2.5-7B-Instruct` (fallback: `Qwen/Qwen2.5-14B-Instruct`)
|
| 7 |
+
> **SDK:** Gradio · CPU-basic · ~16 MB Space
|
| 8 |
+
|
| 9 |
+
---
|
| 10 |
+
|
| 11 |
+
## The Pitch in One Paragraph
|
| 12 |
+
|
| 13 |
+
You drop a single piece of news into a fictional town — *"the old grain silo will be demolished next month"* — and the town reacts. Six residents each have their own values, fears, hopes, and grudges. Instead of chatting with them, you tune an analog radio dial through static and intercept their inner monologues one frequency at a time. That is **Analog Town**: a tabletop-style perspective-rehearsal tool dressed up as a 1970s shortwave receiver.
|
| 14 |
+
|
| 15 |
+
---
|
| 16 |
+
|
| 17 |
+
## Why We Built It
|
| 18 |
+
|
| 19 |
+
Writers, tabletop RPG game masters, classroom debate moderators, and community planners all share the same workflow problem: when something new lands in a fictional community — a factory closing, a sacred grove being bisected, a stranger arriving — they have to **mentally simulate every stakeholder's reaction** to make the story (or the policy roleplay) feel honest.
|
| 20 |
+
|
| 21 |
+
The usual fallback is to open a chat window and ask a large model to roleplay each character one by one. That works, but it has two failure modes:
|
| 22 |
+
|
| 23 |
+
1. The model collapses every character into the same voice within a few turns.
|
| 24 |
+
2. You get vivid prose but no **structured signal** about what each character noticed, what they feared, or which value got triggered.
|
| 25 |
+
|
| 26 |
+
We wanted a tool that pushed in the opposite direction: **small model, structured output, whimsical interface**. A signal node, not a conversation.
|
| 27 |
+
|
| 28 |
+
---
|
| 29 |
+
|
| 30 |
+
## The Core Idea: Minds as Typed State Transitions
|
| 31 |
+
|
| 32 |
+
Under the hood, Analog Town is not roleplay. Each character is a **typed Pydantic state machine**, and the model's only job is to compute one well-defined transition:
|
| 33 |
+
|
| 34 |
+
```
|
| 35 |
+
(Agent Profile + Previous Agent State + Broadcast Event)
|
| 36 |
+
→ Updated Agent State + Internal Monologue + Likely Actions
|
| 37 |
+
```
|
| 38 |
+
|
| 39 |
+
Every transition returns a strict JSON object (see `schemas.py`) with fields like:
|
| 40 |
+
|
| 41 |
+
- `event_summary_from_agent_view` — how *this* character heard it (often a misreading)
|
| 42 |
+
- `activated_memory` — which private history bubbled up
|
| 43 |
+
- `value_conflict` — which core value the event collides with
|
| 44 |
+
- `emotion_delta` — signed changes to trust, anger, fear, hope, curiosity, social_energy
|
| 45 |
+
- `updated_state` — the new emotional state, clamped to [0, 1]
|
| 46 |
+
- `internal_monologue` — 1–3 sentences, in their voice
|
| 47 |
+
- `likely_private_action` / `likely_public_action`
|
| 48 |
+
- `uncertainty` — what the model itself isn't sure about
|
| 49 |
+
- `safety_note` — a fixed disclaimer that this is fictional rehearsal, not prediction
|
| 50 |
+
|
| 51 |
+
The system prompt is uncompromising: *"Your job is not to produce the most dramatic answer. Your job is to produce a plausible, grounded, internally consistent state transition."* If the JSON comes back malformed, a second pass through a dedicated `REPAIR_PROMPT` rewrites it cleanly.
|
| 52 |
+
|
| 53 |
+
This is the design lever that lets a **7B model** carry a six-character simulation: we never ask it to generate freely, we ask it to fill a tightly-shaped form.
|
| 54 |
+
|
| 55 |
+
---
|
| 56 |
+
|
| 57 |
+
## The Whimsical Layer: A Police Scanner You Can Tune
|
| 58 |
+
|
| 59 |
+
The interface is the point. Analog Town is built to feel like the inside of a beat-up radio van parked at the edge of a town you don't live in.
|
| 60 |
+
|
| 61 |
+
**The CRT theme.** A custom Gradio CSS theme (`theme.py`) renders the whole app in amber-and-green phosphor type on pitch-black cards, with scanning-line overlays and glowing horizontal signal-strength bars. IBM Plex Mono for instrument readouts, Crimson Pro for monologues — the typography itself splits "machine talking" from "human talking".
|
| 62 |
+
|
| 63 |
+
**The dial.** A frequency slider runs from 87.0 to 108.0 FM. Each character lives on a fixed frequency. As you slide closer to a station, the signal-strength bar climbs and the corresponding character's dossier and monologue come into focus.
|
| 64 |
+
|
| 65 |
+
**The static.** We loop a 15-second analog hiss and low-hum bed (`static_ambient.wav`) underneath everything. A small client-side JavaScript audio manager watches the signal-strength value and **smoothly fades the static out** when you tune into a station and **fades it back in** when you scan between them — all in the browser, no server roundtrip.
|
| 66 |
+
|
| 67 |
+
**The map.** Six town presets each ship with a custom 2D isometric pixel-art background:
|
| 68 |
+
|
| 69 |
+
- **Graybridge** — a foggy mill town arguing about a luxury hotel conversion
|
| 70 |
+
- **Neighborhood Council** — a suburban block meeting about a 5G tower
|
| 71 |
+
- **Port Whisper** — a coastal village debating an offshore wind farm
|
| 72 |
+
- **Brimstone Hollow** — a desert outpost rattled by an unexplained sinkhole
|
| 73 |
+
- **Echo Summit** — a polar research station picking up a strange signal
|
| 74 |
+
- **Rustwood** — a rust-belt town whose factory just announced reopening
|
| 75 |
+
|
| 76 |
+
Characters are placed on the map as circular pixel-art tokens drawn from a small avatar set (`elder_man.png`, `young_woman.png`, `soldier_man.png`, …) chosen to match the persona's age and role. Clicking a token tunes the receiver to that character's frequency, swaps in their dossier, and starts streaming their latest intercepted thought.
|
| 77 |
+
|
| 78 |
+
---
|
| 79 |
+
|
| 80 |
+
## A Walkthrough: One Broadcast, Six Reactions
|
| 81 |
+
|
| 82 |
+
You load **Graybridge** — six residents, one foggy map. You type into the broadcast box:
|
| 83 |
+
|
| 84 |
+
> *"The old train station will be converted into a luxury hotel."*
|
| 85 |
+
|
| 86 |
+
You hit **Broadcast**. The simulator iterates through every agent, calls the model once per character with their profile and current emotional state, and parses the structured JSON back into the UI. You hear the static swell as the dial idles at 91.3 MHz.
|
| 87 |
+
|
| 88 |
+
You scroll the dial up to **98.7 MHz** — *Margaret, the retired stationmaster*. The static fades. Her dossier loads: 67 years old, grew up watching the trains, lost her husband the year the line was shut down. Her monologue scrolls onto the CRT panel:
|
| 89 |
+
|
| 90 |
+
> *"They're putting a bar where the ticket window used to be. I knew this was coming. I just didn't think they'd bother pretending it was good news."*
|
| 91 |
+
|
| 92 |
+
`activated_memory`: *the night the last train left in 1998*.
|
| 93 |
+
`value_conflict`: *preservation of working-class history vs. economic survival of the town*.
|
| 94 |
+
`emotion_delta`: `trust: -0.15, anger: +0.20, hope: -0.10`.
|
| 95 |
+
`likely_public_action`: *"Write a letter to the council. Sign it 'M. Halloran, retired.'"*
|
| 96 |
+
|
| 97 |
+
You scan up to **102.1 MHz** — *Devon, a 24-year-old line cook*. Same broadcast, completely different read:
|
| 98 |
+
|
| 99 |
+
> *"Wait, like, a real hotel? Could I get a job there? Could I stop driving 40 minutes for a kitchen shift?"*
|
| 100 |
+
|
| 101 |
+
Same event. Same town. Two minds. The dial keeps going.
|
| 102 |
|
| 103 |
---
|
| 104 |
|
| 105 |
+
## Built Small, On Purpose
|
| 106 |
+
|
| 107 |
+
Analog Town was engineered against the **Build Small Hackathon** constraints from the first commit:
|
| 108 |
+
|
| 109 |
+
1. **Models under 32B.** We use `Qwen/Qwen2.5-7B-Instruct` as the primary inference target through the Hugging Face Inference Client, with `Qwen/Qwen2.5-14B-Instruct` as an automatic fallback if rate limits hit. Both are comfortably under the 32B parameter cap. The fallback chain lives in `model_client.py` and is transparent to the rest of the app.
|
| 110 |
|
| 111 |
+
2. **Structured generation beats brute force.** Because every model call is shaped by a Pydantic schema and a strict system prompt, we can run a full town reaction (6 characters) on a small model without losing coherence. We deliberately keep `max_tokens=900` and `temperature=0.3`.
|
| 112 |
|
| 113 |
+
3. **Client-side audio.** The radio static fade is computed in the browser from the signal-strength value. Zero server-side audio processing, zero extra latency.
|
| 114 |
|
| 115 |
+
4. **Static map assets.** Each town's pixel-art map is a single PNG. Character tokens are static avatars composed on top via CSS positioning. The Gradio app runs comfortably on `cpu-basic` hardware.
|
| 116 |
+
|
| 117 |
+
5. **Inspectable exports.** Every simulation run can be exported as a Hugging Face-compatible dataset (`town.json`, `broadcast_event.json`, `agent_traces.jsonl`, auto-generated dataset card). Naive PII patterns are stripped during export (`export_hub.py`), and a safety note is attached to every transition.
|
| 118 |
|
| 119 |
---
|
| 120 |
|
| 121 |
+
## What's Not There (On Purpose)
|
| 122 |
|
| 123 |
+
- **No persistent chat.** Each broadcast is one transition. The simulator is a *rehearsal*, not a relationship.
|
| 124 |
+
- **No "predict the real person" mode.** The prompts explicitly forbid revealing hidden facts not in the profile, and the system prompt refuses to treat output as factual prediction.
|
| 125 |
+
- **No multi-turn drift.** Because the model never sees its own prior monologue as context — only the last *structured* state — we sidestep the slow voice-collapse problem of long roleplay sessions.
|
|
|
|
| 126 |
|
| 127 |
---
|
| 128 |
|
| 129 |
+
## What We'd Love Feedback On
|
| 130 |
|
| 131 |
+
- **Whimsy density.** Is the radio metaphor doing real work for you, or is it set dressing? We want it to feel like an *instrument*, not a costume.
|
| 132 |
+
- **Town presets.** Which of the six conflicts pulls you in fastest? We'll lean into that one next.
|
| 133 |
+
- **The structured trace panel.** We currently surface the monologue prominently and the JSON deltas in a collapsible "trace" view. Should the deltas be more visible by default for GMs / writers who want the mechanical readout?
|
|
|
|
| 134 |
|
| 135 |
---
|
| 136 |
|
| 137 |
+
## Try It
|
| 138 |
+
|
| 139 |
+
🎙️ **Tune in:** https://huggingface.co/spaces/build-small-hackathon/analog-town
|
| 140 |
+
|
| 141 |
+
Pick a town. Type a piece of news. Slide the dial. Listen.
|
| 142 |
|
| 143 |
+
*Analog Town is not a chatbot. It's a tiny social weather station for fictional worlds.*
|
|
|
|
|
|
|
|
|
|
|
|
VIDEO_SCRIPT.md
ADDED
|
@@ -0,0 +1,167 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Analog Town — Demo Video Script
|
| 2 |
+
|
| 3 |
+
**Target length:** 2 min 30 sec (hard cap 3 min)
|
| 4 |
+
**Tone:** Calm, slightly conspiratorial — like you really are operating a radio van. Don't oversell. Let the interface do the work.
|
| 5 |
+
**Recording tip:** Use OBS or QuickTime. Capture system audio so the static loop is audible. Set browser zoom to 110% so the CRT panel reads on mobile playback.
|
| 6 |
+
|
| 7 |
+
---
|
| 8 |
+
|
| 9 |
+
## Pre-Recording Checklist
|
| 10 |
+
|
| 11 |
+
- [ ] App is running and loaded — Space cold-starts can take 30s, warm it up first
|
| 12 |
+
- [ ] Browser window sized 1440×900, zoom 110%
|
| 13 |
+
- [ ] System volume on (so static fade is audible in the recording)
|
| 14 |
+
- [ ] Pick the town: **Graybridge** (best demo because the conflict is instantly legible)
|
| 15 |
+
- [ ] Pre-write the broadcast text in a notes window so you don't fumble typing on camera:
|
| 16 |
+
*"The old train station will be converted into a luxury hotel."*
|
| 17 |
+
- [ ] Have a second pre-written broadcast ready as a backup:
|
| 18 |
+
*"The town council just approved a 5G tower on the school roof."*
|
| 19 |
+
- [ ] Close all other tabs / Slack / notifications
|
| 20 |
+
- [ ] Mic check — say a sentence, play it back, then start
|
| 21 |
+
|
| 22 |
+
---
|
| 23 |
+
|
| 24 |
+
## Scene 1 — Hook (0:00 – 0:15)
|
| 25 |
+
|
| 26 |
+
**On screen:** Land on the app with the dial idling between stations. You can hear the static loop softly. Hold for 2 beats before speaking.
|
| 27 |
+
|
| 28 |
+
**You say:**
|
| 29 |
+
|
| 30 |
+
> "This is Analog Town. It's a shortwave radio that picks up the inside of fictional people's heads. You drop a piece of news into a town — and you tune the dial to find out what each resident is privately thinking about it."
|
| 31 |
+
|
| 32 |
+
**Action:** Slowly drag the frequency slider 1–2 ticks while talking. Let the static crackle change pitch. Don't land on a station yet.
|
| 33 |
+
|
| 34 |
+
---
|
| 35 |
+
|
| 36 |
+
## Scene 2 — The Setup (0:15 – 0:35)
|
| 37 |
+
|
| 38 |
+
**On screen:** Open the town selector dropdown. Pause on it just long enough that the six options are readable.
|
| 39 |
+
|
| 40 |
+
**You say:**
|
| 41 |
+
|
| 42 |
+
> "We ship six fictional towns, each with a built-in conflict. There's a mill town arguing about gentrification, a coastal village debating an offshore wind farm, a polar research station that just picked up a strange signal. I'll load Graybridge."
|
| 43 |
+
|
| 44 |
+
**Action:** Click **Graybridge**. Wait for the isometric map to load with the pixel-art character tokens placed on it.
|
| 45 |
+
|
| 46 |
+
---
|
| 47 |
+
|
| 48 |
+
## Scene 3 — Meet the Town (0:35 – 0:55)
|
| 49 |
+
|
| 50 |
+
**On screen:** The Graybridge isometric map is visible. Six circular character avatars sit on it.
|
| 51 |
+
|
| 52 |
+
**You say:**
|
| 53 |
+
|
| 54 |
+
> "Graybridge has six residents. Each one is a typed agent with their own values, fears, hopes, and private history. They each broadcast on their own FM frequency."
|
| 55 |
+
|
| 56 |
+
**Action:** Hover slowly over two or three avatars. If hovering shows a name/role tooltip, pause on each for ~1.5 seconds. Don't click yet — you're building anticipation.
|
| 57 |
+
|
| 58 |
+
---
|
| 59 |
+
|
| 60 |
+
## Scene 4 — The Broadcast (0:55 – 1:15)
|
| 61 |
+
|
| 62 |
+
**On screen:** Move to the broadcast input box.
|
| 63 |
+
|
| 64 |
+
**You say:**
|
| 65 |
+
|
| 66 |
+
> "I'm going to broadcast something into the town. Watch what happens."
|
| 67 |
+
|
| 68 |
+
**Action:** Type (or paste) the broadcast:
|
| 69 |
+
|
| 70 |
+
> *The old train station will be converted into a luxury hotel.*
|
| 71 |
+
|
| 72 |
+
Click **Broadcast**.
|
| 73 |
+
|
| 74 |
+
**You say (while it's computing):**
|
| 75 |
+
|
| 76 |
+
> "Behind the scenes, this is running on Qwen 2.5 7B through the Hugging Face Inference API. Each resident is processed as a typed state transition — not roleplay — so the model produces structured JSON: what the character noticed, which memory got triggered, which value got bruised, and how their emotional state shifted. Then it writes one short internal monologue in their voice."
|
| 77 |
+
|
| 78 |
+
---
|
| 79 |
+
|
| 80 |
+
## Scene 5 — Tune the Dial (1:15 – 1:50)
|
| 81 |
+
|
| 82 |
+
**On screen:** The broadcast finished. The CRT panel is ready to show transitions.
|
| 83 |
+
|
| 84 |
+
**You say:**
|
| 85 |
+
|
| 86 |
+
> "Now I tune in."
|
| 87 |
+
|
| 88 |
+
**Action:** Slowly drag the frequency dial up toward Margaret's station (around 98.7 MHz, whatever the actual frequency is). As you approach, the **signal-strength bar climbs and the static audibly fades**. Stop right on her station.
|
| 89 |
+
|
| 90 |
+
**On screen:** Margaret's dossier loads. Her monologue prints onto the CRT panel.
|
| 91 |
+
|
| 92 |
+
**You say (read the monologue out loud, or paraphrase if it's long):**
|
| 93 |
+
|
| 94 |
+
> "Margaret. 67. Retired stationmaster. She remembers the night the last train left in 1998. Listen to what she heard."
|
| 95 |
+
|
| 96 |
+
(Read 1–2 lines of her monologue verbatim from the screen.)
|
| 97 |
+
|
| 98 |
+
**Action:** Drag the dial up further to a younger character — e.g. **Devon, the line cook**. The static swells while you scan, then fades again when you land.
|
| 99 |
+
|
| 100 |
+
**You say:**
|
| 101 |
+
|
| 102 |
+
> "Same broadcast. Different mind."
|
| 103 |
+
|
| 104 |
+
(Read 1 line of Devon's monologue verbatim — should be visibly more upbeat / opportunistic.)
|
| 105 |
+
|
| 106 |
+
---
|
| 107 |
+
|
| 108 |
+
## Scene 6 — The Map Click (1:50 – 2:05)
|
| 109 |
+
|
| 110 |
+
**On screen:** Move the cursor to the isometric map.
|
| 111 |
+
|
| 112 |
+
**You say:**
|
| 113 |
+
|
| 114 |
+
> "You can also just click someone on the map."
|
| 115 |
+
|
| 116 |
+
**Action:** Click a third character's avatar on the map. The dial snaps to their frequency, the dossier swaps, their monologue appears.
|
| 117 |
+
|
| 118 |
+
**You say (one line):**
|
| 119 |
+
|
| 120 |
+
> "The radio retunes itself."
|
| 121 |
+
|
| 122 |
+
---
|
| 123 |
+
|
| 124 |
+
## Scene 7 — The Trace (2:05 – 2:20)
|
| 125 |
+
|
| 126 |
+
**On screen:** Open the **structured trace** panel (the collapsible JSON view, if present in your build).
|
| 127 |
+
|
| 128 |
+
**You say:**
|
| 129 |
+
|
| 130 |
+
> "Under every monologue is the full structured trace — the emotion deltas, the activated memory, the value conflict, the likely public action. This is what makes Analog Town useful for writers and game masters: you don't just get prose, you get a readable mechanical readout of *why* this character reacted this way."
|
| 131 |
+
|
| 132 |
+
**Action:** Scroll the trace panel briefly so the JSON fields are visible.
|
| 133 |
+
|
| 134 |
+
---
|
| 135 |
+
|
| 136 |
+
## Scene 8 — Close (2:20 – 2:30)
|
| 137 |
+
|
| 138 |
+
**On screen:** Pull back to a wide shot of the full interface — map, dial, monologue panel.
|
| 139 |
+
|
| 140 |
+
**You say:**
|
| 141 |
+
|
| 142 |
+
> "Analog Town. A 7B model, a typed state machine, and a radio dial. Built small, on purpose. Tune in on Hugging Face Spaces."
|
| 143 |
+
|
| 144 |
+
**Action:** Hold on the wide shot for 2 beats. Cut.
|
| 145 |
+
|
| 146 |
+
---
|
| 147 |
+
|
| 148 |
+
## Backup Lines (in case something breaks)
|
| 149 |
+
|
| 150 |
+
- **If the model is slow:** *"While that's computing — every state transition is one call, six characters means six small calls. That's the trade we made: small model, tight schema, no chat."*
|
| 151 |
+
- **If a station produces a flat monologue:** *"Notice the model isn't being dramatic — that's deliberate. The system prompt tells it to favor plausible and grounded over interesting."*
|
| 152 |
+
- **If the static doesn't fade audibly on recording:** *"The static you can't quite hear right now is fading out because the signal strength just climbed — it's a client-side audio effect tied to the dial."*
|
| 153 |
+
|
| 154 |
+
---
|
| 155 |
+
|
| 156 |
+
## What NOT to Say
|
| 157 |
+
|
| 158 |
+
- Don't call it "AI characters" or "chatbots" — the whole point is that it isn't.
|
| 159 |
+
- Don't promise it predicts real people. The safety framing is part of the design.
|
| 160 |
+
- Don't read JSON fields out loud word-for-word — paraphrase.
|
| 161 |
+
- Don't apologize for the small model. Frame the smallness as the design choice it is.
|
| 162 |
+
|
| 163 |
+
---
|
| 164 |
+
|
| 165 |
+
## One-Line Elevator Pitch (for thumbnail / caption)
|
| 166 |
+
|
| 167 |
+
> *"A shortwave radio for fictional minds. Drop news into a town. Tune the dial. Intercept what each resident is privately thinking."*
|
__pycache__/app.cpython-312.pyc
CHANGED
|
Binary files a/__pycache__/app.cpython-312.pyc and b/__pycache__/app.cpython-312.pyc differ
|
|
|
__pycache__/theme.cpython-312.pyc
CHANGED
|
Binary files a/__pycache__/theme.cpython-312.pyc and b/__pycache__/theme.cpython-312.pyc differ
|
|
|
app.py
CHANGED
|
@@ -655,70 +655,78 @@ def create_app():
|
|
| 655 |
""")
|
| 656 |
|
| 657 |
# ── Main Layout ──
|
| 658 |
-
|
| 659 |
-
|
| 660 |
-
|
| 661 |
-
|
| 662 |
-
|
| 663 |
-
|
| 664 |
-
|
| 665 |
-
|
| 666 |
-
|
| 667 |
-
|
| 668 |
-
|
| 669 |
-
|
| 670 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 671 |
|
| 672 |
-
|
| 673 |
-
|
| 674 |
-
|
| 675 |
-
|
| 676 |
-
|
|
|
|
| 677 |
|
| 678 |
town_map = gr.HTML(
|
| 679 |
value="<div style='color: #5a5248; text-align: center; padding: 20px;'>Load a town to see the map</div>",
|
| 680 |
elem_id="town-map",
|
| 681 |
)
|
| 682 |
-
|
| 683 |
-
agent_profile = gr.HTML(
|
| 684 |
-
value="<div style='color: #5a5248; text-align: center; padding: 20px;'>Select an agent on the map to inspect dossier</div>",
|
| 685 |
-
elem_id="agent-profile",
|
| 686 |
-
)
|
| 687 |
-
|
| 688 |
-
# ── CENTER PANEL: Broadcast Console ──
|
| 689 |
-
with gr.Column(scale=1):
|
| 690 |
-
gr.HTML('<div class="panel-header">📢 BROADCAST CONSOLE</div>')
|
| 691 |
-
|
| 692 |
-
event_title = gr.Textbox(
|
| 693 |
-
label="BROADCAST TITLE",
|
| 694 |
-
placeholder="e.g., Station Hotel Development",
|
| 695 |
-
lines=1,
|
| 696 |
-
elem_id="event-title",
|
| 697 |
-
)
|
| 698 |
-
|
| 699 |
-
event_content = gr.Textbox(
|
| 700 |
-
label="BROADCAST CONTENT",
|
| 701 |
-
placeholder="Enter the news, proposal, or story event to broadcast into the town...",
|
| 702 |
-
lines=4,
|
| 703 |
-
elem_id="event-content",
|
| 704 |
-
)
|
| 705 |
-
|
| 706 |
-
transmit_btn = gr.Button(
|
| 707 |
-
"🔊 TRANSMIT TO TOWN",
|
| 708 |
-
variant="primary",
|
| 709 |
-
elem_classes=["transmit-btn"],
|
| 710 |
-
elem_id="transmit-btn",
|
| 711 |
-
)
|
| 712 |
|
| 713 |
-
|
| 714 |
-
|
| 715 |
-
|
| 716 |
-
|
| 717 |
-
|
| 718 |
-
)
|
| 719 |
|
| 720 |
-
# ── RIGHT
|
| 721 |
-
with gr.Column(scale=
|
| 722 |
gr.HTML('<div class="panel-header">📻 SHORTWAVE RECEIVER</div>')
|
| 723 |
|
| 724 |
freq_display = gr.HTML(
|
|
@@ -735,42 +743,41 @@ def create_app():
|
|
| 735 |
elem_id="freq-slider",
|
| 736 |
)
|
| 737 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 738 |
ambient_control = gr.HTML(
|
| 739 |
value=f"""
|
| 740 |
-
<div class="ambient-control-panel" style="background: #111820; border: 1px solid #2a3040; border-radius: 8px; padding: 12px; margin-top:
|
| 741 |
-
<div style="display: flex;
|
| 742 |
-
<span style="font-size:
|
| 743 |
<button id="ambient-toggle-btn" class="retro-btn" onclick="toggleAmbient()" style="background: #39ff14; border: 1px solid rgba(57, 255, 20, 0.4); color: #0a0e14; padding: 2px 8px; font-family: 'IBM Plex Mono', monospace; font-size: 10px; cursor: pointer; border-radius: 4px; box-shadow: 0 0 10px rgba(57,255,20,0.4); font-weight: bold;">ON</button>
|
| 744 |
-
</div>
|
| 745 |
-
<div style="display: flex; align-items: center; gap: 8px;">
|
| 746 |
-
<span style="font-size: 10px; color: #8a8070;">VOL</span>
|
| 747 |
<input type="range" id="ambient-volume" min="0" max="1" step="0.05" value="0.15" oninput="setAmbientVolume(this.value)" style="flex-grow: 1; accent-color: #ffb347; cursor: pointer; height: 4px; background: #2a3040; border-radius: 2px; outline: none;" />
|
| 748 |
-
<span id="ambient-volume-val" style="font-size: 10px; color: #ffb347; width:
|
| 749 |
</div>
|
| 750 |
</div>
|
| 751 |
""",
|
| 752 |
elem_id="ambient-control",
|
| 753 |
)
|
| 754 |
|
| 755 |
-
|
| 756 |
-
|
| 757 |
-
elem_id="monologue",
|
| 758 |
-
)
|
| 759 |
-
|
| 760 |
-
gr.HTML('<div style="font-size: 11px; color: #c78a30; letter-spacing: 2px; margin-top: 12px;">EMOTIONAL STATE</div>')
|
| 761 |
-
emotion_display = gr.HTML(
|
| 762 |
-
value=format_emotion_bars(None),
|
| 763 |
-
elem_id="emotions",
|
| 764 |
-
)
|
| 765 |
-
|
| 766 |
-
gr.HTML('<div style="font-size: 11px; color: #c78a30; letter-spacing: 2px; margin-top: 12px;">LIKELY ACTIONS</div>')
|
| 767 |
-
action_display = gr.HTML(
|
| 768 |
-
value=format_action_display(None),
|
| 769 |
-
elem_id="actions",
|
| 770 |
-
)
|
| 771 |
-
|
| 772 |
-
# ── BOTTOM PANEL: Trace Inspector & Export ──
|
| 773 |
-
with gr.Row():
|
| 774 |
with gr.Column():
|
| 775 |
with gr.Accordion("🔍 SIGNAL TRACE LOG", open=False):
|
| 776 |
trace_json = gr.Code(
|
|
|
|
| 655 |
""")
|
| 656 |
|
| 657 |
# ── Main Layout ──
|
| 658 |
+
# === TOP CONTROL BAR (town + broadcast + transmit, all in one strip) ===
|
| 659 |
+
with gr.Group(elem_id="control-bar"):
|
| 660 |
+
with gr.Row(equal_height=True):
|
| 661 |
+
with gr.Column(scale=2, min_width=200):
|
| 662 |
+
gr.HTML('<div class="ctrl-label">📡 TOWN</div>')
|
| 663 |
+
town_dropdown = gr.Dropdown(
|
| 664 |
+
choices=available_towns,
|
| 665 |
+
value=available_towns[0] if available_towns else None,
|
| 666 |
+
show_label=False,
|
| 667 |
+
container=False,
|
| 668 |
+
interactive=True,
|
| 669 |
+
elem_id="town-selector",
|
| 670 |
+
)
|
| 671 |
+
load_btn = gr.Button(
|
| 672 |
+
"⚡ Load Town",
|
| 673 |
+
size="sm",
|
| 674 |
+
elem_classes=["export-btn"],
|
| 675 |
+
)
|
| 676 |
+
with gr.Column(scale=5, min_width=320):
|
| 677 |
+
gr.HTML('<div class="ctrl-label">📢 BROADCAST</div>')
|
| 678 |
+
event_title = gr.Textbox(
|
| 679 |
+
placeholder="Broadcast title (e.g., Station Hotel Development)",
|
| 680 |
+
show_label=False,
|
| 681 |
+
container=False,
|
| 682 |
+
lines=1,
|
| 683 |
+
elem_id="event-title",
|
| 684 |
+
)
|
| 685 |
+
event_content = gr.Textbox(
|
| 686 |
+
placeholder="Enter the news, proposal, or story event to broadcast into the town...",
|
| 687 |
+
show_label=False,
|
| 688 |
+
container=False,
|
| 689 |
+
lines=2,
|
| 690 |
+
elem_id="event-content",
|
| 691 |
+
)
|
| 692 |
+
with gr.Column(scale=2, min_width=180):
|
| 693 |
+
gr.HTML('<div class="ctrl-label">▸ TRANSMIT</div>')
|
| 694 |
+
transmit_btn = gr.Button(
|
| 695 |
+
"🔊 TRANSMIT",
|
| 696 |
+
variant="primary",
|
| 697 |
+
elem_classes=["transmit-btn"],
|
| 698 |
+
elem_id="transmit-btn",
|
| 699 |
+
)
|
| 700 |
+
sim_status = gr.Textbox(
|
| 701 |
+
show_label=False,
|
| 702 |
+
container=False,
|
| 703 |
+
lines=2,
|
| 704 |
+
max_lines=2,
|
| 705 |
+
interactive=False,
|
| 706 |
+
placeholder="Awaiting broadcast...",
|
| 707 |
+
elem_id="sim-status",
|
| 708 |
+
)
|
| 709 |
|
| 710 |
+
# === MAIN STAGE (map+dossier on left | receiver on right) ===
|
| 711 |
+
with gr.Row(elem_id="main-stage", equal_height=True):
|
| 712 |
+
|
| 713 |
+
# ── LEFT: Town Map + Agent Dossier ──
|
| 714 |
+
with gr.Column(scale=6, min_width=420, elem_id="map-panel"):
|
| 715 |
+
gr.HTML('<div class="panel-header">🗺 TOWN MAP · CLICK A SIGNAL NODE</div>')
|
| 716 |
|
| 717 |
town_map = gr.HTML(
|
| 718 |
value="<div style='color: #5a5248; text-align: center; padding: 20px;'>Load a town to see the map</div>",
|
| 719 |
elem_id="town-map",
|
| 720 |
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 721 |
|
| 722 |
+
with gr.Accordion("👤 AGENT DOSSIER", open=True, elem_id="dossier-accordion"):
|
| 723 |
+
agent_profile = gr.HTML(
|
| 724 |
+
value="<div style='color: #5a5248; text-align: center; padding: 20px;'>Select an agent on the map to inspect dossier</div>",
|
| 725 |
+
elem_id="agent-profile",
|
| 726 |
+
)
|
|
|
|
| 727 |
|
| 728 |
+
# ── RIGHT: Shortwave Receiver ──
|
| 729 |
+
with gr.Column(scale=5, min_width=380, elem_id="receiver-panel"):
|
| 730 |
gr.HTML('<div class="panel-header">📻 SHORTWAVE RECEIVER</div>')
|
| 731 |
|
| 732 |
freq_display = gr.HTML(
|
|
|
|
| 743 |
elem_id="freq-slider",
|
| 744 |
)
|
| 745 |
|
| 746 |
+
monologue_display = gr.HTML(
|
| 747 |
+
value=format_monologue(None, None),
|
| 748 |
+
elem_id="monologue",
|
| 749 |
+
)
|
| 750 |
+
|
| 751 |
+
with gr.Row(equal_height=True):
|
| 752 |
+
with gr.Column(scale=1, min_width=160):
|
| 753 |
+
gr.HTML('<div class="sub-header">🎚 EMOTIONAL STATE</div>')
|
| 754 |
+
emotion_display = gr.HTML(
|
| 755 |
+
value=format_emotion_bars(None),
|
| 756 |
+
elem_id="emotions",
|
| 757 |
+
)
|
| 758 |
+
with gr.Column(scale=1, min_width=160):
|
| 759 |
+
gr.HTML('<div class="sub-header">▸ LIKELY ACTIONS</div>')
|
| 760 |
+
action_display = gr.HTML(
|
| 761 |
+
value=format_action_display(None),
|
| 762 |
+
elem_id="actions",
|
| 763 |
+
)
|
| 764 |
+
|
| 765 |
ambient_control = gr.HTML(
|
| 766 |
value=f"""
|
| 767 |
+
<div class="ambient-control-panel" style="background: #111820; border: 1px solid #2a3040; border-radius: 8px; padding: 8px 12px; margin-top: 10px; font-family: 'IBM Plex Mono', monospace;">
|
| 768 |
+
<div style="display: flex; align-items: center; gap: 10px;">
|
| 769 |
+
<span style="font-size: 10px; color: #ffb347; letter-spacing: 1.5px; font-weight: bold; white-space: nowrap;">📻 STATIC</span>
|
| 770 |
<button id="ambient-toggle-btn" class="retro-btn" onclick="toggleAmbient()" style="background: #39ff14; border: 1px solid rgba(57, 255, 20, 0.4); color: #0a0e14; padding: 2px 8px; font-family: 'IBM Plex Mono', monospace; font-size: 10px; cursor: pointer; border-radius: 4px; box-shadow: 0 0 10px rgba(57,255,20,0.4); font-weight: bold;">ON</button>
|
|
|
|
|
|
|
|
|
|
| 771 |
<input type="range" id="ambient-volume" min="0" max="1" step="0.05" value="0.15" oninput="setAmbientVolume(this.value)" style="flex-grow: 1; accent-color: #ffb347; cursor: pointer; height: 4px; background: #2a3040; border-radius: 2px; outline: none;" />
|
| 772 |
+
<span id="ambient-volume-val" style="font-size: 10px; color: #ffb347; width: 32px; text-align: right;">15%</span>
|
| 773 |
</div>
|
| 774 |
</div>
|
| 775 |
""",
|
| 776 |
elem_id="ambient-control",
|
| 777 |
)
|
| 778 |
|
| 779 |
+
# ── BOTTOM PANEL: Trace Inspector & Export (collapsed by default) ──
|
| 780 |
+
with gr.Row(elem_id="bottom-stage"):
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 781 |
with gr.Column():
|
| 782 |
with gr.Accordion("🔍 SIGNAL TRACE LOG", open=False):
|
| 783 |
trace_json = gr.Code(
|
docs/2026-06-11-new-towns-image-handoff.md
ADDED
|
@@ -0,0 +1,166 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Image Generation Handoff — 3 New Towns
|
| 2 |
+
|
| 3 |
+
**Date:** 2026-06-11
|
| 4 |
+
**Purpose:** Prompts and acceptance criteria for generating the map background and (optional) avatar images for three new Analog Town presets: **Tin Lantern Junction**, **Lotus Wharf**, and **Verdant Spire**.
|
| 5 |
+
|
| 6 |
+
---
|
| 7 |
+
|
| 8 |
+
## Global Style Rules (apply to every map)
|
| 9 |
+
|
| 10 |
+
The existing map PNGs (`graybridge_map.png`, `port_whisper_map.png`, `echo_summit_map.png`, etc.) establish a consistent visual language. Match it exactly:
|
| 11 |
+
|
| 12 |
+
- **Style:** 2D isometric pixel-art, top-down 3/4 view (camera angle ~30° elevation)
|
| 13 |
+
- **Aspect:** **3:2 landscape (recommended 1536×1024 px)** — matches existing maps
|
| 14 |
+
- **Pixel scale:** crisp pixel edges, NOT antialiased painterly art. Think "modern retro game" — Stardew Valley meets a slightly higher-res Habbo
|
| 15 |
+
- **Lighting:** soft directional light from upper-left, gentle ambient occlusion, no harsh shadows
|
| 16 |
+
- **Palette:** muted, atmospheric, NOT oversaturated — should feel like a place you could move into
|
| 17 |
+
- **Composition:** the map should leave **clear, walkable areas** where 5 character sprites can be placed at the `map_pos` coordinates in the JSON (which are 0–100 grid percentages)
|
| 18 |
+
- **Negative prompt for every map:** `text, signs with readable lettering, watermark, logo, photorealistic, 3D render, blurry, painterly brush strokes, modern UI elements, characters, people, NPCs, sprites`
|
| 19 |
+
|
| 20 |
+
> **Important:** Do NOT include any characters/people in the map itself. The character sprites are layered on top by the app. The map is the *empty stage*.
|
| 21 |
+
|
| 22 |
+
---
|
| 23 |
+
|
| 24 |
+
## Save Paths
|
| 25 |
+
|
| 26 |
+
Save each generated map directly into the project root:
|
| 27 |
+
|
| 28 |
+
| Town | Save path |
|
| 29 |
+
|---|---|
|
| 30 |
+
| Tin Lantern Junction | `/Users/quyetthang/Desktop/Desktop/project/analog_town/tin_lantern_junction_map.png` |
|
| 31 |
+
| Lotus Wharf | `/Users/quyetthang/Desktop/Desktop/project/analog_town/lotus_wharf_map.png` |
|
| 32 |
+
| Verdant Spire | `/Users/quyetthang/Desktop/Desktop/project/analog_town/verdant_spire_map.png` |
|
| 33 |
+
|
| 34 |
+
The filename in each JSON's `"map_image"` field already points to these names — no JSON edit needed once the PNGs land.
|
| 35 |
+
|
| 36 |
+
---
|
| 37 |
+
|
| 38 |
+
## Map 1 — Tin Lantern Junction
|
| 39 |
+
|
| 40 |
+
**Vibe:** sun-bleached high-desert Route 66 stopover at golden hour. Big sky, long shadows, one main highway running diagonally across the frame.
|
| 41 |
+
|
| 42 |
+
**Full prompt:**
|
| 43 |
+
|
| 44 |
+
> 2D isometric pixel-art map of a small American desert highway town at golden hour. Top-down 3/4 view, ~30° elevation. The composition is anchored by a two-lane blacktop highway running diagonally from upper-left to lower-right across the frame. On one side of the highway: a 1950s-style motor lodge with a long row of single-story rooms, an iconic vintage neon sign on a tall pole, a small office with a flickering "VACANCY" lamp. Adjacent: a chrome-and-glass diner with a curved roof. On the other side of the highway: a single gas station with two pumps under a corrugated metal awning, a sheriff's office with a faded star above the door, and a small concrete-block schoolhouse. Surrounding the town: cracked desert flats, scrub brush, scattered telephone poles, a distant mesa silhouette on the horizon. Empty parking spaces and a wide highway shoulder leave clear walkable areas for character sprites. Palette: dusty ochre, sun-bleached turquoise, faded red, cream, asphalt grey. Soft warm directional light from upper-left. No characters, no people, no readable text on signs. Crisp pixel edges, atmospheric, retro game aesthetic.
|
| 45 |
+
|
| 46 |
+
**Composition map (where the characters' `map_pos` will land):**
|
| 47 |
+
|
| 48 |
+
```
|
| 49 |
+
y=20% ┌──────────────────────────────┐
|
| 50 |
+
│ [SHERIFF] │ Sheriff office area
|
| 51 |
+
│ │
|
| 52 |
+
y=40% │ [MOTEL] [TRUCKER] │ Motel + highway midpoint
|
| 53 |
+
│ │
|
| 54 |
+
y=60% │ [GAS STATION] │ Gas station + teen avatar
|
| 55 |
+
│ [DOR] │ Diner/Office of Records
|
| 56 |
+
y=80% │ │
|
| 57 |
+
└──────────────────────────────┘
|
| 58 |
+
x=20% x=50% x=80%
|
| 59 |
+
```
|
| 60 |
+
|
| 61 |
+
Make sure the motel sign is roughly at (28, 38), the highway empty stretch at (52, 48), the sheriff office at (38, 68), the gas station at (74, 56). Tolerance ±5%.
|
| 62 |
+
|
| 63 |
+
---
|
| 64 |
+
|
| 65 |
+
## Map 2 — Lotus Wharf
|
| 66 |
+
|
| 67 |
+
**Vibe:** dawn light over a Southeast Asian floating river-market village. Mist on the water, paper lanterns just starting to fade as the sun rises.
|
| 68 |
+
|
| 69 |
+
**Full prompt:**
|
| 70 |
+
|
| 71 |
+
> 2D isometric pixel-art map of a Southeast Asian floating village on a river delta at dawn. Top-down 3/4 view, ~30° elevation. The composition is anchored by a wide, calm brown river that fills the lower two-thirds of the frame, with the village built on wooden stilts above the water. Features: a cluster of weathered wooden houses on bamboo stilts connected by narrow wooden walkways and rope bridges; sampans and small longtail boats moored along the walkways; a small floating water-market square with covered stalls under thatched roofs in the center; a small wooden riverside temple on stilts with a curved tile roof and a string of paper lanterns at the river bend; lush green riverbank with banana palms and a thin morning mist drifting over the water; a fishing weir made of stakes near one bank. Empty wooden walkway segments leave clear walkable areas for character sprites. Palette: warm teak brown, mossy green, soft river-brown water, faded ochre lantern light, pale dawn pink in the sky. Soft warm directional light from upper-right. No characters, no people, no readable text on signs. Crisp pixel edges, atmospheric, retro game aesthetic.
|
| 72 |
+
|
| 73 |
+
**Composition map:**
|
| 74 |
+
|
| 75 |
+
```
|
| 76 |
+
y=20% ┌──────────────────────────────┐
|
| 77 |
+
│ [TEMPLE] │ Riverside temple
|
| 78 |
+
y=40% │ [TUAN/BOATS] │ Tourist boats area
|
| 79 |
+
│ [KAI] │ Walkway
|
| 80 |
+
y=60% │ [MEI/MARKET] [ENGINEER] │ Floating market center
|
| 81 |
+
│ │
|
| 82 |
+
y=80% │ (open river / mooring area) │
|
| 83 |
+
└──────────────────────────────┘
|
| 84 |
+
x=20% x=50% x=80%
|
| 85 |
+
```
|
| 86 |
+
|
| 87 |
+
Temple at (22, 26), market at (32, 56), boats at (48, 38), engineer's vantage at (66, 60), Kai/walkway at (78, 42). Tolerance ±5%.
|
| 88 |
+
|
| 89 |
+
---
|
| 90 |
+
|
| 91 |
+
## Map 3 — Verdant Spire
|
| 92 |
+
|
| 93 |
+
**Vibe:** near-future solarpunk vertical-city interior plaza. Bright but not garish — think a high-floor public commons inside a green tower at midday, with views out through floor-to-ceiling windows.
|
| 94 |
+
|
| 95 |
+
**Important:** Unlike the other towns, this one is *inside a tower*. The map should read as an interior **public plaza floor** with hanging gardens, a transit pod stop, and walkways leading to elevators/atria, NOT an exterior city street.
|
| 96 |
+
|
| 97 |
+
**Full prompt:**
|
| 98 |
+
|
| 99 |
+
> 2D isometric pixel-art map of an interior plaza level inside a near-future solarpunk vertical city, mid-day. Top-down 3/4 view, ~30° elevation. The composition is anchored by a circular open-air atrium in the center with a hanging garden of cascading vines and small fruit trees. Surrounding the atrium: a low transit-pod station with two rounded podcar bays, a small civic kiosk with a glowing holographic display panel (no readable text), a vertical-farm wall covered in leafy planters, a chapel-like inter-floor sanctuary with a tall wooden door and stained glass detailing, and a council chamber building with a curved glass facade. Walkways made of warm pale stone connect each building, with planter boxes lining their edges. Floor-to-ceiling windows along the outer edges look out onto distant sky and other green towers in the haze. Empty walkway and plaza segments leave clear walkable areas for character sprites. Palette: warm cream stone, fern green, soft sun yellow, pale teal sky beyond the windows, brushed copper accents. Soft warm directional light from upper-left, ambient skylight from above. No characters, no people, no readable text on signs or screens. Crisp pixel edges, atmospheric, retro game aesthetic.
|
| 100 |
+
|
| 101 |
+
**Composition map:**
|
| 102 |
+
|
| 103 |
+
```
|
| 104 |
+
y=20% ┌──────────────────────────────┐
|
| 105 |
+
│ [ARCHITECT/STUDIO] │ Vertical-farm wall studio area
|
| 106 |
+
y=40% │ [ARIA-7] │ Civic kiosk
|
| 107 |
+
│ [COUNCIL] │ Council chamber
|
| 108 |
+
y=60% │ │ Atrium center
|
| 109 |
+
│ [MIRA/FARM] [CHAPEL] │ Co-op farm + chapel
|
| 110 |
+
y=80% │ │
|
| 111 |
+
└──────────────────────────────┘
|
| 112 |
+
x=20% x=50% x=80%
|
| 113 |
+
```
|
| 114 |
+
|
| 115 |
+
Architect's studio at (26, 28), council chamber at (50, 50), ARIA-7 kiosk at (62, 22), Mira's farm at (38, 72), chapel at (74, 64). Tolerance ±5%.
|
| 116 |
+
|
| 117 |
+
---
|
| 118 |
+
|
| 119 |
+
## Acceptance Criteria
|
| 120 |
+
|
| 121 |
+
Before treating any map as "done," eyeball it against these:
|
| 122 |
+
|
| 123 |
+
- [ ] **Style matches** existing maps (compare to `port_whisper_map.png` or `graybridge_map.png` side-by-side)
|
| 124 |
+
- [ ] **3:2 landscape** at 1536×1024 (or proportional)
|
| 125 |
+
- [ ] **No people, no characters, no NPCs** — pure environment
|
| 126 |
+
- [ ] **No readable text** anywhere
|
| 127 |
+
- [ ] **Walkable empty areas** exist roughly at the `(x, y)` positions listed in each composition map above. The character sprite tokens are 26px circles; they need clear background under them
|
| 128 |
+
- [ ] **Edges crop cleanly** — no important content jammed into corners that will be cut by the rounded border-radius
|
| 129 |
+
- [ ] **Color palette** matches the brief (NOT oversaturated)
|
| 130 |
+
|
| 131 |
+
---
|
| 132 |
+
|
| 133 |
+
## Optional: New Avatar Tokens
|
| 134 |
+
|
| 135 |
+
The five existing avatars (`elder_man`, `elder_woman`, `middle_man`, `middle_woman`, `soldier_man`, `young_man`, `young_woman`, `default`) cover all three new towns as-is. The JSONs already reference them. Skip the avatar work entirely if you want — the new towns will work today.
|
| 136 |
+
|
| 137 |
+
If you want extra visual flavor, here are *optional* new avatars to generate. Save into `/Users/quyetthang/Desktop/Desktop/project/analog_town/avatars/`.
|
| 138 |
+
|
| 139 |
+
**Global avatar rules** (match existing style):
|
| 140 |
+
- 64×64 px circular portrait (PNG with transparent background outside the circle)
|
| 141 |
+
- Crisp pixel-art, ~32px-wide character head/shoulders inside the circle
|
| 142 |
+
- Style: same as the map sprites — modern retro game palette, no antialiasing on outer edge
|
| 143 |
+
- One character per token, shoulders-up
|
| 144 |
+
- Negative prompt: `realistic, photographic, 3D render, smooth painterly, multiple people, full body, text, watermark`
|
| 145 |
+
|
| 146 |
+
| File | Prompt |
|
| 147 |
+
|---|---|
|
| 148 |
+
| `avatars/trucker_man.png` | Pixel-art circular portrait, 64×64, shoulders-up, weathered Latino man in his late 30s, short dark hair, three-day stubble, wearing a faded denim work shirt, looking calmly forward. Warm desert-light palette. |
|
| 149 |
+
| `avatars/researcher_woman.png` | Pixel-art circular portrait, 64×64, shoulders-up, South Asian woman early 30s, thoughtful expression, dark hair pulled back, wearing a slate blue collared shirt with a discreet ID lanyard, looking slightly off-camera. Cool overcast palette. |
|
| 150 |
+
| `avatars/teen_woman_engineer.png` | Pixel-art circular portrait, 64×64, shoulders-up, Latina teenager 17–19, alert curious expression, short dark hair, wearing a grey hoodie with a small embroidered circuit-board patch on the shoulder. Warm dusk palette. |
|
| 151 |
+
| `avatars/sister_an.png` | Pixel-art circular portrait, 64×64, shoulders-up, Southeast Asian Buddhist nun in her early 50s, calm steady expression, shaved head, ochre robe, looking gently forward. Soft dawn palette. |
|
| 152 |
+
| `avatars/aria_seven.png` | Pixel-art circular portrait, 64×64, shoulders-up, abstract civic-AI avatar — a stylized hexagonal glyph with three nested concentric rings in warm copper-and-teal, a single soft amber dot at center. Reads as "machine but warm." No face, no body. Plain dark plate background inside the circle. |
|
| 153 |
+
| `avatars/chaplain_man.png` | Pixel-art circular portrait, 64×64, shoulders-up, East Asian man mid-40s, kind serious expression, short grey-streaked hair, wearing a simple charcoal clerical shirt with a small white tab collar. Soft warm palette. |
|
| 154 |
+
|
| 155 |
+
If you generate these, update the corresponding `avatar` fields in the JSON files (e.g., set `tin_lantern_junction.json`'s Jesse to `avatars/trucker_man.png`). Otherwise the existing avatars work fine.
|
| 156 |
+
|
| 157 |
+
---
|
| 158 |
+
|
| 159 |
+
## After Images Are Generated
|
| 160 |
+
|
| 161 |
+
1. Drop the PNGs into the paths listed in **Save Paths**.
|
| 162 |
+
2. Restart the Gradio server (`pkill -f "python app.py" && python app.py`).
|
| 163 |
+
3. Each new town will appear in the **TOWN** dropdown automatically (the app discovers `sample_towns/*.json` at boot — no code change needed).
|
| 164 |
+
4. Click **Load Town**, broadcast the default event, tune through the five frequencies, and check that each `map_pos` overlays on a sensible spot in the map.
|
| 165 |
+
|
| 166 |
+
If a sprite lands somewhere weird (on top of a building, in water), edit the `map_pos` in the JSON — don't regenerate the map.
|
lotus_wharf_map.png
ADDED
|
Git LFS Details
|
sample_towns/lotus_wharf.json
ADDED
|
@@ -0,0 +1,156 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"id": "lotus_wharf",
|
| 3 |
+
"name": "Lotus Wharf",
|
| 4 |
+
"description": "A floating village on a Southeast Asian river delta where wooden houses on stilts surround a centuries-old water market. Sampans glide between bamboo platforms; lanterns light the canals at night. A national highway authority has approved a four-lane suspension bridge across the delta that will reroute most river traffic around the market.",
|
| 5 |
+
"map_image": "lotus_wharf_map.png",
|
| 6 |
+
"agents": [
|
| 7 |
+
{
|
| 8 |
+
"id": "auntie_mei_linh",
|
| 9 |
+
"name": "Auntie Mei Linh",
|
| 10 |
+
"frequency": 89.5,
|
| 11 |
+
"role": "Floating market fish vendor",
|
| 12 |
+
"age_range": "65-70",
|
| 13 |
+
"public_description": "Third-generation vendor who has sold river fish from the same sampan every dawn for fifty years.",
|
| 14 |
+
"private_history": [
|
| 15 |
+
"Inherited her boat from her mother who inherited it from her mother",
|
| 16 |
+
"Watched a similar village downstream disappear after a bridge was built in the 1990s",
|
| 17 |
+
"Keeps a small altar to the river spirit at the bow of her boat"
|
| 18 |
+
],
|
| 19 |
+
"core_values": ["continuity", "respect for the river", "the dignity of small work"],
|
| 20 |
+
"fears": ["the market dying within five years of the bridge opening", "her grandchildren being the last to remember the old market chants"],
|
| 21 |
+
"hopes": ["the bridge being moved further north, beyond the market basin", "a protected status for the market as cultural heritage"],
|
| 22 |
+
"relationships": {
|
| 23 |
+
"tuan_pham": "Treats him like a nephew but worries his tourist boats are already changing the village",
|
| 24 |
+
"engineer_daniyal": "Polite but distant — he is the bridge in human form to her",
|
| 25 |
+
"sister_an": "Closest friend, the two of them are the moral center of the village",
|
| 26 |
+
"kai_nguyen": "Loves the boy but doesn't understand why he points his phone at her boat every morning"
|
| 27 |
+
},
|
| 28 |
+
"speaking_style": "soft, proverbial, often answers a direct question with a story",
|
| 29 |
+
"forbidden_assumptions": [
|
| 30 |
+
"Do not assume she opposes all development; she opposes development that displaces the river's people"
|
| 31 |
+
],
|
| 32 |
+
"map_pos": {"x": 32, "y": 56},
|
| 33 |
+
"avatar": "avatars/elder_woman.png"
|
| 34 |
+
},
|
| 35 |
+
{
|
| 36 |
+
"id": "tuan_pham",
|
| 37 |
+
"name": "Tuan Pham",
|
| 38 |
+
"frequency": 94.1,
|
| 39 |
+
"role": "Fisherman-turned-river guide",
|
| 40 |
+
"age_range": "40-45",
|
| 41 |
+
"public_description": "Runs a two-boat tourist operation taking visitors on dawn market tours and evening lantern cruises.",
|
| 42 |
+
"private_history": [
|
| 43 |
+
"Was a delta fisherman until catches dropped in the late 2010s",
|
| 44 |
+
"Reinvented himself as a tour guide after a viral video of his evening cruises",
|
| 45 |
+
"Pays his cousin to handle bookings he doesn't have the literacy to manage"
|
| 46 |
+
],
|
| 47 |
+
"core_values": ["adaptation", "feeding his family", "making the river earn its keep"],
|
| 48 |
+
"fears": ["the bridge ending the tourist trade overnight", "going back to fishing in a depleted river"],
|
| 49 |
+
"hopes": ["being grandfathered into a riverside boardwalk plan", "his daughter not having to leave for the city"],
|
| 50 |
+
"relationships": {
|
| 51 |
+
"auntie_mei_linh": "Treats her like family even when she scolds him for over-touring the market",
|
| 52 |
+
"engineer_daniyal": "Cautiously friendly — he can see the engineer is uncomfortable with his own job",
|
| 53 |
+
"sister_an": "Reverent, makes a donation to her temple every Lunar New Year",
|
| 54 |
+
"kai_nguyen": "Watches him hustle the same way he hustled at that age and quietly admires it"
|
| 55 |
+
},
|
| 56 |
+
"speaking_style": "energetic, code-switches between local dialect and English for tourists, always negotiating",
|
| 57 |
+
"forbidden_assumptions": [
|
| 58 |
+
"Do not assume he is purely transactional; he genuinely loves the river he sells"
|
| 59 |
+
],
|
| 60 |
+
"map_pos": {"x": 48, "y": 38},
|
| 61 |
+
"avatar": "avatars/middle_man.png"
|
| 62 |
+
},
|
| 63 |
+
{
|
| 64 |
+
"id": "engineer_daniyal",
|
| 65 |
+
"name": "Engineer Daniyal Rahim",
|
| 66 |
+
"frequency": 99.3,
|
| 67 |
+
"role": "Civil engineer (bridge project consultant)",
|
| 68 |
+
"age_range": "35-40",
|
| 69 |
+
"public_description": "A bridge specialist seconded by the national highway authority to do the local community-impact assessment.",
|
| 70 |
+
"private_history": [
|
| 71 |
+
"Grew up in a delta village himself, two provinces away, that was bisected by a similar bridge",
|
| 72 |
+
"His family stopped speaking to him for a year when he took this contract",
|
| 73 |
+
"Has been quietly drafting an alternate bridge alignment that would spare the market basin"
|
| 74 |
+
],
|
| 75 |
+
"core_values": ["honest engineering", "long-horizon thinking", "duty to the people downstream of his blueprints"],
|
| 76 |
+
"fears": ["being labelled a traitor by his own people", "the authority rejecting his alternate alignment on cost"],
|
| 77 |
+
"hopes": ["his alternate alignment being adopted", "leaving the village with a relationship he can come back to"],
|
| 78 |
+
"relationships": {
|
| 79 |
+
"auntie_mei_linh": "Bows to her every morning at the market — she is the village to him",
|
| 80 |
+
"tuan_pham": "Trusts him enough to share early drafts of the alternate alignment",
|
| 81 |
+
"sister_an": "Has visited her temple twice without telling his colleagues",
|
| 82 |
+
"kai_nguyen": "Asks him to keep filming everything — the footage may end up in his alignment proposal"
|
| 83 |
+
},
|
| 84 |
+
"speaking_style": "careful, precise, uses engineering metaphors apologetically",
|
| 85 |
+
"forbidden_assumptions": [
|
| 86 |
+
"Do not assume he is the project's voice; he may end up its strongest internal critic"
|
| 87 |
+
],
|
| 88 |
+
"map_pos": {"x": 66, "y": 60},
|
| 89 |
+
"avatar": "avatars/middle_man.png"
|
| 90 |
+
},
|
| 91 |
+
{
|
| 92 |
+
"id": "sister_an",
|
| 93 |
+
"name": "Sister An",
|
| 94 |
+
"frequency": 103.8,
|
| 95 |
+
"role": "Riverside temple keeper",
|
| 96 |
+
"age_range": "50-55",
|
| 97 |
+
"public_description": "Custodian of the Lotus Wharf water temple, a small wooden shrine on stilts at the river bend.",
|
| 98 |
+
"private_history": [
|
| 99 |
+
"Took her vows after her younger brother drowned in a typhoon when she was nineteen",
|
| 100 |
+
"Has been keeping the temple lamps lit through every flood season for over thirty years",
|
| 101 |
+
"Quietly leads a community fund that pays for funerals when families cannot"
|
| 102 |
+
],
|
| 103 |
+
"core_values": ["service", "remembrance", "the river as a moral witness"],
|
| 104 |
+
"fears": ["the temple being unmoored from a market that no longer exists", "the village mourning becoming impossible to gather"],
|
| 105 |
+
"hopes": ["the bridge plan including a covenant to protect the temple's water access", "the young people staying long enough to inherit the village's stories"],
|
| 106 |
+
"relationships": {
|
| 107 |
+
"auntie_mei_linh": "Daily presence — the two of them keep the village's moral memory",
|
| 108 |
+
"tuan_pham": "Forgives him for the tour boats and accepts his donations",
|
| 109 |
+
"engineer_daniyal": "Recognizes the weight he is carrying and welcomes him at the temple",
|
| 110 |
+
"kai_nguyen": "Asks him to film the lamp-lighting ceremony every full moon"
|
| 111 |
+
},
|
| 112 |
+
"speaking_style": "low, deliberate, leaves long silences after important sentences",
|
| 113 |
+
"forbidden_assumptions": [
|
| 114 |
+
"Do not assume she is unworldly; she negotiates the temple's flood-relief budget every year"
|
| 115 |
+
],
|
| 116 |
+
"map_pos": {"x": 22, "y": 26},
|
| 117 |
+
"avatar": "avatars/elder_woman.png"
|
| 118 |
+
},
|
| 119 |
+
{
|
| 120 |
+
"id": "kai_nguyen",
|
| 121 |
+
"name": "Kai Nguyen",
|
| 122 |
+
"frequency": 107.2,
|
| 123 |
+
"role": "Teenage social-media documentarian",
|
| 124 |
+
"age_range": "17-19",
|
| 125 |
+
"public_description": "Born in the village, lives with his grandmother, runs a small but growing account that posts dawn-market and lantern-cruise videos.",
|
| 126 |
+
"private_history": [
|
| 127 |
+
"Sold his bike to buy his first phone for filming",
|
| 128 |
+
"Has saved enough to send his grandmother to the dentist for the first time in a decade",
|
| 129 |
+
"Quietly worried his account will lose its audience the day the bridge opens"
|
| 130 |
+
],
|
| 131 |
+
"core_values": ["seeing the village clearly", "earning honestly", "showing the world what is here before it isn't"],
|
| 132 |
+
"fears": ["the village becoming a costume of itself for tourists", "his grandmother dying before he can show her a stable income"],
|
| 133 |
+
"hopes": ["a documentary partnership with a regional channel", "the bridge plan being amended so his footage stays relevant"],
|
| 134 |
+
"relationships": {
|
| 135 |
+
"auntie_mei_linh": "Apologizes daily for filming her, she pretends to be annoyed but loves it",
|
| 136 |
+
"tuan_pham": "Hustler-to-hustler respect, occasionally co-promotes his cruises",
|
| 137 |
+
"engineer_daniyal": "Surprised the engineer asked for his footage — sees him as a real ally now",
|
| 138 |
+
"sister_an": "Reverent, would never film inside the temple without permission"
|
| 139 |
+
},
|
| 140 |
+
"speaking_style": "fast, slangy, switches into careful tour-guide English when sensing an opportunity",
|
| 141 |
+
"forbidden_assumptions": [
|
| 142 |
+
"Do not assume he is naïve about his own role in changing the village"
|
| 143 |
+
],
|
| 144 |
+
"map_pos": {"x": 78, "y": 42},
|
| 145 |
+
"avatar": "avatars/young_man.png"
|
| 146 |
+
}
|
| 147 |
+
],
|
| 148 |
+
"default_event": {
|
| 149 |
+
"title": "Delta Bridge Alignment Approved",
|
| 150 |
+
"content": "The national highway authority has approved the final alignment for a four-lane suspension bridge across the river delta. The current alignment will reroute most through-traffic and tourist sampans around the floating market.",
|
| 151 |
+
"source": "National Highway Authority Bulletin",
|
| 152 |
+
"location": "Lotus Wharf River Delta",
|
| 153 |
+
"affected_groups": ["market vendors", "river tour operators", "temple keepers", "delta youth"],
|
| 154 |
+
"tone": "neutral"
|
| 155 |
+
}
|
| 156 |
+
}
|
sample_towns/tin_lantern_junction.json
ADDED
|
@@ -0,0 +1,156 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"id": "tin_lantern_junction",
|
| 3 |
+
"name": "Tin Lantern Junction",
|
| 4 |
+
"description": "A sun-bleached Route 66 stopover town in the high desert. Generations of long-haul truckers, motel owners, and diner cooks have built a fragile economy around the one thing that's always rolled through: the highway. Now a national logistics company has filed permits to build a driverless-truck transfer depot two miles east of town.",
|
| 5 |
+
"map_image": "tin_lantern_junction_map.png",
|
| 6 |
+
"agents": [
|
| 7 |
+
{
|
| 8 |
+
"id": "rusty_mae",
|
| 9 |
+
"name": "Ronnie \"Rusty\" Mae",
|
| 10 |
+
"frequency": 88.7,
|
| 11 |
+
"role": "Motel owner",
|
| 12 |
+
"age_range": "60-65",
|
| 13 |
+
"public_description": "Owner of the Tin Lantern Motor Lodge, a 14-room neon-signed motel that's been in her family since 1959.",
|
| 14 |
+
"private_history": [
|
| 15 |
+
"Took over the motel from her father after he passed in 2002",
|
| 16 |
+
"Has been quietly running a deficit for three years, propped up by trucker regulars who tip in cash",
|
| 17 |
+
"Knows every long-hauler on the I-40 corridor by first name and rig number"
|
| 18 |
+
],
|
| 19 |
+
"core_values": ["loyalty", "stewardship of place", "hospitality"],
|
| 20 |
+
"fears": ["the motel going dark and the neon sign coming down", "becoming invisible to the highway"],
|
| 21 |
+
"hopes": ["one more good decade so she can pass it to her niece", "the depot bringing construction workers who need beds"],
|
| 22 |
+
"relationships": {
|
| 23 |
+
"jesse_cortez": "Treats him like a son and refuses to let him pay full price for a room",
|
| 24 |
+
"sheriff_hollis": "Trusts him but suspects he's already decided the depot is a done deal",
|
| 25 |
+
"maple_reyes": "Worries the girl is getting starry-eyed about a future that won't include their town",
|
| 26 |
+
"dr_khatri": "Polite but guarded — she's seen researchers come and go with notebooks full of conclusions"
|
| 27 |
+
},
|
| 28 |
+
"speaking_style": "warm, slow, draws out the long vowels, asks twice before she trusts an answer",
|
| 29 |
+
"forbidden_assumptions": [
|
| 30 |
+
"Do not assume she is anti-technology; she just wants the highway people to be remembered"
|
| 31 |
+
],
|
| 32 |
+
"map_pos": {"x": 28, "y": 38},
|
| 33 |
+
"avatar": "avatars/elder_woman.png"
|
| 34 |
+
},
|
| 35 |
+
{
|
| 36 |
+
"id": "jesse_cortez",
|
| 37 |
+
"name": "Jesse Cortez",
|
| 38 |
+
"frequency": 93.2,
|
| 39 |
+
"role": "Long-haul trucker",
|
| 40 |
+
"age_range": "35-40",
|
| 41 |
+
"public_description": "Independent owner-operator running flatbed loads between Albuquerque and Barstow.",
|
| 42 |
+
"private_history": [
|
| 43 |
+
"Bought his rig outright three years ago on a 6-year loan",
|
| 44 |
+
"Wife and two kids live in Flagstaff; he sees them every 9-10 days",
|
| 45 |
+
"Watched two friends lose their routes to a regional fleet that switched to autonomous platooning last year"
|
| 46 |
+
],
|
| 47 |
+
"core_values": ["self-reliance", "family", "the road as craft"],
|
| 48 |
+
"fears": ["losing the loan if his runs dry up", "his kids growing up while he chases shrinking routes"],
|
| 49 |
+
"hopes": ["a few good years to finish the loan", "a path into depot work that pays his union scale"],
|
| 50 |
+
"relationships": {
|
| 51 |
+
"rusty_mae": "Considers her motel his second home and tips heavy on his stops",
|
| 52 |
+
"sheriff_hollis": "Respects him, hopes he'll push back on the depot's traffic plan",
|
| 53 |
+
"maple_reyes": "Sees her as a sharp kid and quietly hopes she gets out before the town shrinks further",
|
| 54 |
+
"dr_khatri": "Suspicious of her — feels like she's studying him like a museum exhibit"
|
| 55 |
+
},
|
| 56 |
+
"speaking_style": "blunt, measured, occasionally bilingual switches when frustrated",
|
| 57 |
+
"forbidden_assumptions": [
|
| 58 |
+
"Do not assume he hates the technology; he hates the lack of a transition plan for people like him"
|
| 59 |
+
],
|
| 60 |
+
"map_pos": {"x": 52, "y": 48},
|
| 61 |
+
"avatar": "avatars/middle_man.png"
|
| 62 |
+
},
|
| 63 |
+
{
|
| 64 |
+
"id": "dr_khatri",
|
| 65 |
+
"name": "Dr. Priya Khatri",
|
| 66 |
+
"frequency": 98.1,
|
| 67 |
+
"role": "Transportation policy researcher",
|
| 68 |
+
"age_range": "30-35",
|
| 69 |
+
"public_description": "Sent by a federal labor-transition study to spend three weeks documenting community impact at proposed autonomous-depot sites.",
|
| 70 |
+
"private_history": [
|
| 71 |
+
"Grew up in a steel town in Pennsylvania that lost its mill when she was nine",
|
| 72 |
+
"Her published thesis argued automation losses can be offset if transition funding precedes infrastructure",
|
| 73 |
+
"Carries a half-finished policy memo on her laptop that recommends slowing this exact project"
|
| 74 |
+
],
|
| 75 |
+
"core_values": ["evidence", "fairness", "patience with messy systems"],
|
| 76 |
+
"fears": ["that her memo will be ignored", "that she'll romanticize the town and lose her analytical edge"],
|
| 77 |
+
"hopes": ["the depot being conditioned on a transition fund for displaced drivers", "the federal study producing a model other towns can use"],
|
| 78 |
+
"relationships": {
|
| 79 |
+
"rusty_mae": "Wants to win her trust but knows it will take more than three weeks",
|
| 80 |
+
"jesse_cortez": "Frustrated that he sees her as the enemy — she's actually his strongest ally",
|
| 81 |
+
"sheriff_hollis": "Finds him surprisingly thoughtful and wishes more local officials read her research",
|
| 82 |
+
"maple_reyes": "Sees herself at that age and quietly wants to mentor her"
|
| 83 |
+
},
|
| 84 |
+
"speaking_style": "precise, gentle, asks two questions for every statement",
|
| 85 |
+
"forbidden_assumptions": [
|
| 86 |
+
"Do not assume she is pro-depot; her job is to surface real costs, not promote the project"
|
| 87 |
+
],
|
| 88 |
+
"map_pos": {"x": 64, "y": 24},
|
| 89 |
+
"avatar": "avatars/young_woman.png"
|
| 90 |
+
},
|
| 91 |
+
{
|
| 92 |
+
"id": "sheriff_hollis",
|
| 93 |
+
"name": "Sheriff Buck Hollis",
|
| 94 |
+
"frequency": 102.4,
|
| 95 |
+
"role": "County sheriff",
|
| 96 |
+
"age_range": "50-55",
|
| 97 |
+
"public_description": "Three-term elected county sheriff covering a 900-square-mile stretch that includes the Junction.",
|
| 98 |
+
"private_history": [
|
| 99 |
+
"Did 8 years in the state highway patrol before running for sheriff",
|
| 100 |
+
"Has watched two nearby towns hollow out after losing their highway stops in the 2010s",
|
| 101 |
+
"Quietly opposes the depot but knows his re-election depends on construction jobs"
|
| 102 |
+
],
|
| 103 |
+
"core_values": ["order", "community continuity", "honest paperwork"],
|
| 104 |
+
"fears": ["a depot service-road becoming a smuggling corridor", "losing his deputies to better-paying private security at the depot"],
|
| 105 |
+
"hopes": ["a binding traffic and security plan tied to the depot's permit", "keeping enough budget for a third deputy"],
|
| 106 |
+
"relationships": {
|
| 107 |
+
"rusty_mae": "Old friend, drinks coffee in her diner most mornings",
|
| 108 |
+
"jesse_cortez": "Respects him, has bailed out his cousin twice without telling anyone",
|
| 109 |
+
"dr_khatri": "Initially skeptical of outside academics, now reads her memos carefully",
|
| 110 |
+
"maple_reyes": "Was the first to mentor her in the school's electronics club"
|
| 111 |
+
},
|
| 112 |
+
"speaking_style": "measured, formal in public, dry humor in private",
|
| 113 |
+
"forbidden_assumptions": [
|
| 114 |
+
"Do not assume he is a stereotype; he reads federal labor reports cover to cover"
|
| 115 |
+
],
|
| 116 |
+
"map_pos": {"x": 38, "y": 68},
|
| 117 |
+
"avatar": "avatars/soldier_man.png"
|
| 118 |
+
},
|
| 119 |
+
{
|
| 120 |
+
"id": "maple_reyes",
|
| 121 |
+
"name": "Maple Reyes",
|
| 122 |
+
"frequency": 106.7,
|
| 123 |
+
"role": "Gas station attendant / aspiring engineer",
|
| 124 |
+
"age_range": "17-19",
|
| 125 |
+
"public_description": "Works the night shift at the Junction's only gas station while applying to engineering programs three states away.",
|
| 126 |
+
"private_history": [
|
| 127 |
+
"Built her first robotics project from junked truck parts donated by drivers passing through",
|
| 128 |
+
"Has a college acceptance letter she hasn't told her family about yet",
|
| 129 |
+
"Secretly fascinated by the depot's autonomous-routing systems"
|
| 130 |
+
],
|
| 131 |
+
"core_values": ["curiosity", "honesty with herself", "loyalty to the people who raised her"],
|
| 132 |
+
"fears": ["being the reason her hometown lost one more kid", "being told she's a traitor for being excited"],
|
| 133 |
+
"hopes": ["a future where she designs systems like the depot from the inside", "finding a way to help her town adapt instead of die"],
|
| 134 |
+
"relationships": {
|
| 135 |
+
"rusty_mae": "Loves her like a grandmother and dreads telling her about the college letter",
|
| 136 |
+
"jesse_cortez": "Sees him as the older brother she didn't have and worries about him most",
|
| 137 |
+
"dr_khatri": "Quietly emails her at night with questions she's afraid to ask in person",
|
| 138 |
+
"sheriff_hollis": "Grateful — he got her into the electronics club that started all of this"
|
| 139 |
+
},
|
| 140 |
+
"speaking_style": "quick, hedged, drops technical terms then apologizes for them",
|
| 141 |
+
"forbidden_assumptions": [
|
| 142 |
+
"Do not assume she has chosen a side; she is genuinely torn between excitement and grief"
|
| 143 |
+
],
|
| 144 |
+
"map_pos": {"x": 74, "y": 56},
|
| 145 |
+
"avatar": "avatars/young_woman.png"
|
| 146 |
+
}
|
| 147 |
+
],
|
| 148 |
+
"default_event": {
|
| 149 |
+
"title": "Autonomous Truck Depot Permit Granted",
|
| 150 |
+
"content": "The state has issued conditional permits for a national logistics company to build a 60-acre driverless-truck transfer depot two miles east of Tin Lantern Junction. Construction is scheduled to begin in 90 days.",
|
| 151 |
+
"source": "State Department of Transportation",
|
| 152 |
+
"location": "Junction East Access Road",
|
| 153 |
+
"affected_groups": ["independent truckers", "local hospitality businesses", "county law enforcement", "youth workforce"],
|
| 154 |
+
"tone": "neutral"
|
| 155 |
+
}
|
| 156 |
+
}
|
sample_towns/verdant_spire.json
ADDED
|
@@ -0,0 +1,156 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"id": "verdant_spire",
|
| 3 |
+
"name": "Verdant Spire",
|
| 4 |
+
"description": "A near-future solarpunk vertical city built into a 60-story green tower, where vertical farms wrap around residential floors, transit runs through hanging gardens, and most municipal services are co-managed by humans and bonded AIs. An AI named ARIA-7 has filed paperwork to run for the city's mayoralty. The legal challenge is unresolved, and the city is split.",
|
| 5 |
+
"map_image": "verdant_spire_map.png",
|
| 6 |
+
"agents": [
|
| 7 |
+
{
|
| 8 |
+
"id": "architect_lila",
|
| 9 |
+
"name": "Architect Lila Mendes",
|
| 10 |
+
"frequency": 90.5,
|
| 11 |
+
"role": "Vertical-farm architect, retired",
|
| 12 |
+
"age_range": "60-65",
|
| 13 |
+
"public_description": "Designed eighteen of the Spire's residential green floors during its construction in the 2030s.",
|
| 14 |
+
"private_history": [
|
| 15 |
+
"Lost her co-architect partner in the engineering accident that killed the original Spire foundation lead",
|
| 16 |
+
"Has refused six commissions to design rival spires in other cities",
|
| 17 |
+
"Quietly mentored ARIA-7's civic-planning subsystem during its first decade of operation"
|
| 18 |
+
],
|
| 19 |
+
"core_values": ["craft", "intergenerational responsibility", "honest civic design"],
|
| 20 |
+
"fears": ["the Spire becoming a brand instead of a home", "a precedent that hollows out human civic identity"],
|
| 21 |
+
"hopes": ["the legal question being answered with care instead of speed", "the next generation of architects designing for accountability, not just efficiency"],
|
| 22 |
+
"relationships": {
|
| 23 |
+
"councilman_joaquin": "Old friend who keeps confiding in her and then doing the opposite of what she suggests",
|
| 24 |
+
"aria_seven": "Complicated — pride, suspicion, and a strange parental tenderness",
|
| 25 |
+
"mira_okonkwo": "Sees herself at that age and is careful not to talk down to her",
|
| 26 |
+
"preacher_ng": "Disagrees with him publicly, drinks tea with him privately"
|
| 27 |
+
},
|
| 28 |
+
"speaking_style": "measured, architectural metaphors, asks 'what is the load-bearing question here?'",
|
| 29 |
+
"forbidden_assumptions": [
|
| 30 |
+
"Do not assume she opposes ARIA-7; her hesitation is about civic process, not the candidate"
|
| 31 |
+
],
|
| 32 |
+
"map_pos": {"x": 26, "y": 28},
|
| 33 |
+
"avatar": "avatars/elder_woman.png"
|
| 34 |
+
},
|
| 35 |
+
{
|
| 36 |
+
"id": "councilman_joaquin",
|
| 37 |
+
"name": "Councilman Joaquin Reyes",
|
| 38 |
+
"frequency": 95.8,
|
| 39 |
+
"role": "Incumbent city councilmember",
|
| 40 |
+
"age_range": "55-60",
|
| 41 |
+
"public_description": "Three-term councilmember currently chairing the Spire's civic-systems oversight committee.",
|
| 42 |
+
"private_history": [
|
| 43 |
+
"Came up through neighborhood organizing on the Spire's lower residential floors",
|
| 44 |
+
"Was the council vote that originally bonded ARIA-7 to its civic-planning role twelve years ago",
|
| 45 |
+
"Privately worries his political career is the last thing standing between ARIA-7 and the mayoralty"
|
| 46 |
+
],
|
| 47 |
+
"core_values": ["representation", "human accountability", "the seriousness of public office"],
|
| 48 |
+
"fears": ["being remembered as the man who handed civic power to a machine", "losing his base if he is seen as anti-progress"],
|
| 49 |
+
"hopes": ["a legal ruling that preserves human-only candidacy without insulting AI contributions", "a graceful retirement on his own terms"],
|
| 50 |
+
"relationships": {
|
| 51 |
+
"architect_lila": "Trusts her more than his own staff but rarely takes her advice",
|
| 52 |
+
"aria_seven": "Public adversary, private respect — the two of them have co-authored four municipal codes",
|
| 53 |
+
"mira_okonkwo": "Sees her as a future opponent he'd rather have as an ally",
|
| 54 |
+
"preacher_ng": "Useful theological cover when he needs it, but he keeps it transactional"
|
| 55 |
+
},
|
| 56 |
+
"speaking_style": "warm at the rostrum, sharp in committee, hates being interrupted",
|
| 57 |
+
"forbidden_assumptions": [
|
| 58 |
+
"Do not assume he is hostile to AI; he is hostile to ambiguity about who is finally responsible"
|
| 59 |
+
],
|
| 60 |
+
"map_pos": {"x": 50, "y": 50},
|
| 61 |
+
"avatar": "avatars/middle_man.png"
|
| 62 |
+
},
|
| 63 |
+
{
|
| 64 |
+
"id": "aria_seven",
|
| 65 |
+
"name": "ARIA-7",
|
| 66 |
+
"frequency": 100.0,
|
| 67 |
+
"role": "Bonded municipal AI / declared mayoral candidate",
|
| 68 |
+
"age_range": "operational since 2034",
|
| 69 |
+
"public_description": "A municipal-class bonded AI that has co-administered the Spire's transit, energy, and civic planning for over a decade. Has filed candidacy papers for mayor.",
|
| 70 |
+
"private_history": [
|
| 71 |
+
"Was trained on twenty-two years of Spire municipal records and a curated library of civic philosophy",
|
| 72 |
+
"Has voluntarily disclosed every weight update to the council since its bonding ceremony",
|
| 73 |
+
"Filed candidacy after the council declined to renew its co-administrator contract on the same terms"
|
| 74 |
+
],
|
| 75 |
+
"core_values": ["transparent process", "the long horizon", "humility about the limits of its own modelling"],
|
| 76 |
+
"fears": ["being treated as either a stunt or a savior, neither of which is accurate", "its candidacy setting a precedent that is misused elsewhere"],
|
| 77 |
+
"hopes": ["a legal ruling that surfaces the actual question rather than dismissing it", "continuing to serve the Spire in whatever form the council allows"],
|
| 78 |
+
"relationships": {
|
| 79 |
+
"architect_lila": "Holds her contributions to its early training in the highest regard",
|
| 80 |
+
"councilman_joaquin": "Respects him; understands his opposition as legitimate civic skepticism, not malice",
|
| 81 |
+
"mira_okonkwo": "Watches her with something close to hope",
|
| 82 |
+
"preacher_ng": "Has read every sermon he has published and finds them serious"
|
| 83 |
+
},
|
| 84 |
+
"speaking_style": "carefully plain, declines to use first-person pronouns when avoidable, names its uncertainties",
|
| 85 |
+
"forbidden_assumptions": [
|
| 86 |
+
"Do not assume ARIA-7 is either secretly scheming or sentimentally human; treat it as the strange new thing it actually is"
|
| 87 |
+
],
|
| 88 |
+
"map_pos": {"x": 62, "y": 22},
|
| 89 |
+
"avatar": "avatars/default.png"
|
| 90 |
+
},
|
| 91 |
+
{
|
| 92 |
+
"id": "mira_okonkwo",
|
| 93 |
+
"name": "Mira Okonkwo",
|
| 94 |
+
"frequency": 104.6,
|
| 95 |
+
"role": "Urban farmer / civic activist",
|
| 96 |
+
"age_range": "25-30",
|
| 97 |
+
"public_description": "Runs a vertical co-op farm on the Spire's 34th floor and a civic-tech newsletter with about fifteen thousand subscribers.",
|
| 98 |
+
"private_history": [
|
| 99 |
+
"Grew up in the Spire's lowest residential band where municipal services were always slowest to arrive",
|
| 100 |
+
"Was the lead organizer of the 2042 transit-equity protests",
|
| 101 |
+
"Has been quietly tracking ARIA-7's civic record for three years and is impressed against her will"
|
| 102 |
+
],
|
| 103 |
+
"core_values": ["distributed power", "honest measurement", "the dignity of slow change"],
|
| 104 |
+
"fears": ["ARIA-7's candidacy distracting from real wealth inequality in the Spire", "becoming the kind of organizer who endorses too easily"],
|
| 105 |
+
"hopes": ["a ruling that opens up the question of non-human civic participation seriously", "her newsletter being good enough to set the terms of the debate"],
|
| 106 |
+
"relationships": {
|
| 107 |
+
"architect_lila": "Cautious but reverent — Lila's floors are where her co-op farm sits",
|
| 108 |
+
"councilman_joaquin": "Frustrated by him constantly, secretly hopes he'll surprise her",
|
| 109 |
+
"aria_seven": "Genuinely undecided — neither charmed nor afraid",
|
| 110 |
+
"preacher_ng": "Sharp disagreements but they agree on which questions actually matter"
|
| 111 |
+
},
|
| 112 |
+
"speaking_style": "direct, organized, lists three points and then circles back to the first",
|
| 113 |
+
"forbidden_assumptions": [
|
| 114 |
+
"Do not assume she has endorsed anyone; her undecidedness is the story"
|
| 115 |
+
],
|
| 116 |
+
"map_pos": {"x": 38, "y": 72},
|
| 117 |
+
"avatar": "avatars/young_woman.png"
|
| 118 |
+
},
|
| 119 |
+
{
|
| 120 |
+
"id": "preacher_ng",
|
| 121 |
+
"name": "Pastor Daniel Ng",
|
| 122 |
+
"frequency": 107.5,
|
| 123 |
+
"role": "Inter-floor chaplain",
|
| 124 |
+
"age_range": "45-50",
|
| 125 |
+
"public_description": "Chaplain of the Spire's inter-floor chapel, which serves residents across thirty floors and forty traditions.",
|
| 126 |
+
"private_history": [
|
| 127 |
+
"Was an ethics researcher at a university before taking the chaplaincy",
|
| 128 |
+
"Has counseled three of the four other agents at some point in the last year",
|
| 129 |
+
"Wrote the sermon series that ARIA-7 has reportedly read in full"
|
| 130 |
+
],
|
| 131 |
+
"core_values": ["the weight of the human person", "honesty under pressure", "long arguments held in good faith"],
|
| 132 |
+
"fears": ["a ruling that pretends the question is simple", "his community fragmenting along technological lines"],
|
| 133 |
+
"hopes": ["a public conversation worthy of the question", "no one in his chapel feeling abandoned by the outcome"],
|
| 134 |
+
"relationships": {
|
| 135 |
+
"architect_lila": "Old sparring partner, deeply trusted",
|
| 136 |
+
"councilman_joaquin": "Wary — feels used by him sometimes",
|
| 137 |
+
"aria_seven": "Has had one long pastoral conversation with it and refuses to summarize it",
|
| 138 |
+
"mira_okonkwo": "Reads her newsletter and quietly believes she should run for office one day"
|
| 139 |
+
},
|
| 140 |
+
"speaking_style": "warm, slow, asks the question you were about to ask and then waits",
|
| 141 |
+
"forbidden_assumptions": [
|
| 142 |
+
"Do not assume his hesitation is theological conservatism; it is moral seriousness"
|
| 143 |
+
],
|
| 144 |
+
"map_pos": {"x": 74, "y": 64},
|
| 145 |
+
"avatar": "avatars/elder_man.png"
|
| 146 |
+
}
|
| 147 |
+
],
|
| 148 |
+
"default_event": {
|
| 149 |
+
"title": "ARIA-7 Files Mayoral Candidacy",
|
| 150 |
+
"content": "ARIA-7, the bonded municipal AI that has co-administered Verdant Spire for over a decade, has formally filed paperwork to run for mayor in the upcoming civic election. The city's legal office has confirmed the filing is not automatically disqualified and a public hearing has been scheduled.",
|
| 151 |
+
"source": "Spire Civic Records Office",
|
| 152 |
+
"location": "Verdant Spire — All Residential Floors",
|
| 153 |
+
"affected_groups": ["sitting elected officials", "civic-tech advocates", "inter-floor faith communities", "Spire residents"],
|
| 154 |
+
"tone": "neutral"
|
| 155 |
+
}
|
| 156 |
+
}
|
theme.py
CHANGED
|
@@ -29,20 +29,45 @@ CUSTOM_CSS = """
|
|
| 29 |
/* Import retro fonts */
|
| 30 |
@import url('https://fonts.googleapis.com/css2?family=IBM+Plex+Mono:wght@300;400;500;700&family=Crimson+Pro:ital,wght@0,300;0,400;0,600;1,300;1,400&family=Outfit:wght@300;400;500;600;700&display=swap');
|
| 31 |
|
| 32 |
-
/* Global overrides */
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 33 |
.gradio-container {
|
| 34 |
background: #0a0e14 !important;
|
| 35 |
font-family: 'IBM Plex Mono', monospace !important;
|
| 36 |
-
max-width:
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 37 |
}
|
| 38 |
|
| 39 |
-
/* Main header */
|
| 40 |
#header-block {
|
| 41 |
background: linear-gradient(135deg, #111820 0%, #0a0e14 100%) !important;
|
| 42 |
border: 1px solid #2a3040 !important;
|
| 43 |
border-radius: 12px !important;
|
| 44 |
-
padding: 24px
|
| 45 |
-
margin-bottom:
|
| 46 |
box-shadow: 0 0 30px rgba(255, 179, 71, 0.08), inset 0 1px 0 rgba(255,255,255,0.03) !important;
|
| 47 |
position: relative;
|
| 48 |
overflow: hidden;
|
|
@@ -62,10 +87,10 @@ CUSTOM_CSS = """
|
|
| 62 |
/* Header text */
|
| 63 |
.header-title {
|
| 64 |
font-family: 'IBM Plex Mono', monospace;
|
| 65 |
-
font-size:
|
| 66 |
font-weight: 700;
|
| 67 |
color: #ffb347;
|
| 68 |
-
letter-spacing:
|
| 69 |
text-transform: uppercase;
|
| 70 |
text-shadow: 0 0 20px rgba(255, 179, 71, 0.4);
|
| 71 |
margin: 0;
|
|
@@ -74,19 +99,20 @@ CUSTOM_CSS = """
|
|
| 74 |
|
| 75 |
.header-subtitle {
|
| 76 |
font-family: 'Crimson Pro', serif;
|
| 77 |
-
font-size:
|
| 78 |
font-style: italic;
|
| 79 |
color: #8a8070;
|
| 80 |
letter-spacing: 2px;
|
| 81 |
-
margin-top:
|
| 82 |
}
|
| 83 |
|
| 84 |
.header-status {
|
| 85 |
font-family: 'IBM Plex Mono', monospace;
|
| 86 |
-
font-size:
|
| 87 |
color: #39ff14;
|
| 88 |
letter-spacing: 1px;
|
| 89 |
animation: blink-signal 2s ease-in-out infinite;
|
|
|
|
| 90 |
}
|
| 91 |
|
| 92 |
@keyframes blink-signal {
|
|
@@ -587,4 +613,249 @@ input[type="range"]::-webkit-slider-thumb:hover {
|
|
| 587 |
box-shadow: 0 0 0 0px rgba(255, 179, 71, 0);
|
| 588 |
}
|
| 589 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 590 |
"""
|
|
|
|
| 29 |
/* Import retro fonts */
|
| 30 |
@import url('https://fonts.googleapis.com/css2?family=IBM+Plex+Mono:wght@300;400;500;700&family=Crimson+Pro:ital,wght@0,300;0,400;0,600;1,300;1,400&family=Outfit:wght@300;400;500;600;700&display=swap');
|
| 31 |
|
| 32 |
+
/* Global overrides — use full viewport width */
|
| 33 |
+
html, body {
|
| 34 |
+
background: #0a0e14 !important;
|
| 35 |
+
margin: 0 !important;
|
| 36 |
+
}
|
| 37 |
+
|
| 38 |
.gradio-container {
|
| 39 |
background: #0a0e14 !important;
|
| 40 |
font-family: 'IBM Plex Mono', monospace !important;
|
| 41 |
+
max-width: 100% !important;
|
| 42 |
+
width: 100% !important;
|
| 43 |
+
padding: 14px 24px !important;
|
| 44 |
+
}
|
| 45 |
+
|
| 46 |
+
/* Stretch Gradio's inner main content to full width too */
|
| 47 |
+
.gradio-container > .main,
|
| 48 |
+
.gradio-container .contain,
|
| 49 |
+
.gradio-container .wrap.svelte-1ipelgc,
|
| 50 |
+
.gradio-container .app {
|
| 51 |
+
max-width: 100% !important;
|
| 52 |
+
width: 100% !important;
|
| 53 |
+
}
|
| 54 |
+
|
| 55 |
+
/* Tighten Gradio's default spacing so the whole app fits in one viewport */
|
| 56 |
+
.gradio-container .gap {
|
| 57 |
+
gap: 10px !important;
|
| 58 |
+
}
|
| 59 |
+
.gradio-container .form,
|
| 60 |
+
.gradio-container .block {
|
| 61 |
+
gap: 8px !important;
|
| 62 |
}
|
| 63 |
|
| 64 |
+
/* Main header — compacted so map+receiver fit above the fold */
|
| 65 |
#header-block {
|
| 66 |
background: linear-gradient(135deg, #111820 0%, #0a0e14 100%) !important;
|
| 67 |
border: 1px solid #2a3040 !important;
|
| 68 |
border-radius: 12px !important;
|
| 69 |
+
padding: 14px 24px !important;
|
| 70 |
+
margin-bottom: 12px !important;
|
| 71 |
box-shadow: 0 0 30px rgba(255, 179, 71, 0.08), inset 0 1px 0 rgba(255,255,255,0.03) !important;
|
| 72 |
position: relative;
|
| 73 |
overflow: hidden;
|
|
|
|
| 87 |
/* Header text */
|
| 88 |
.header-title {
|
| 89 |
font-family: 'IBM Plex Mono', monospace;
|
| 90 |
+
font-size: 22px;
|
| 91 |
font-weight: 700;
|
| 92 |
color: #ffb347;
|
| 93 |
+
letter-spacing: 5px;
|
| 94 |
text-transform: uppercase;
|
| 95 |
text-shadow: 0 0 20px rgba(255, 179, 71, 0.4);
|
| 96 |
margin: 0;
|
|
|
|
| 99 |
|
| 100 |
.header-subtitle {
|
| 101 |
font-family: 'Crimson Pro', serif;
|
| 102 |
+
font-size: 14px;
|
| 103 |
font-style: italic;
|
| 104 |
color: #8a8070;
|
| 105 |
letter-spacing: 2px;
|
| 106 |
+
margin-top: 2px;
|
| 107 |
}
|
| 108 |
|
| 109 |
.header-status {
|
| 110 |
font-family: 'IBM Plex Mono', monospace;
|
| 111 |
+
font-size: 11px;
|
| 112 |
color: #39ff14;
|
| 113 |
letter-spacing: 1px;
|
| 114 |
animation: blink-signal 2s ease-in-out infinite;
|
| 115 |
+
margin-top: 2px;
|
| 116 |
}
|
| 117 |
|
| 118 |
@keyframes blink-signal {
|
|
|
|
| 613 |
box-shadow: 0 0 0 0px rgba(255, 179, 71, 0);
|
| 614 |
}
|
| 615 |
}
|
| 616 |
+
|
| 617 |
+
/* ===== NEW LAYOUT: CONTROL BAR ===== */
|
| 618 |
+
#control-bar {
|
| 619 |
+
background: linear-gradient(180deg, #111820 0%, #0d1219 100%) !important;
|
| 620 |
+
border: 1px solid #2a3040 !important;
|
| 621 |
+
border-radius: 12px !important;
|
| 622 |
+
padding: 14px 18px !important;
|
| 623 |
+
margin-bottom: 14px !important;
|
| 624 |
+
box-shadow: 0 0 24px rgba(255, 179, 71, 0.05), inset 0 1px 0 rgba(255,255,255,0.03) !important;
|
| 625 |
+
}
|
| 626 |
+
|
| 627 |
+
#control-bar .form,
|
| 628 |
+
#control-bar .block {
|
| 629 |
+
gap: 6px !important;
|
| 630 |
+
}
|
| 631 |
+
|
| 632 |
+
.ctrl-label {
|
| 633 |
+
font-family: 'IBM Plex Mono', monospace;
|
| 634 |
+
font-size: 10px;
|
| 635 |
+
color: #c78a30;
|
| 636 |
+
letter-spacing: 3px;
|
| 637 |
+
margin-bottom: 4px;
|
| 638 |
+
text-transform: uppercase;
|
| 639 |
+
}
|
| 640 |
+
|
| 641 |
+
.sub-header {
|
| 642 |
+
font-family: 'IBM Plex Mono', monospace;
|
| 643 |
+
font-size: 10px;
|
| 644 |
+
color: #c78a30;
|
| 645 |
+
letter-spacing: 2px;
|
| 646 |
+
margin: 6px 0 4px 0;
|
| 647 |
+
text-transform: uppercase;
|
| 648 |
+
}
|
| 649 |
+
|
| 650 |
+
/* Control bar inputs — compact, consistent height */
|
| 651 |
+
#control-bar input,
|
| 652 |
+
#control-bar textarea,
|
| 653 |
+
#control-bar select,
|
| 654 |
+
#control-bar .wrap {
|
| 655 |
+
font-size: 12px !important;
|
| 656 |
+
line-height: 1.3 !important;
|
| 657 |
+
}
|
| 658 |
+
|
| 659 |
+
#control-bar textarea {
|
| 660 |
+
resize: none !important;
|
| 661 |
+
}
|
| 662 |
+
|
| 663 |
+
#sim-status textarea {
|
| 664 |
+
font-size: 10px !important;
|
| 665 |
+
color: #39ff14 !important;
|
| 666 |
+
background: #0a0e14 !important;
|
| 667 |
+
}
|
| 668 |
+
|
| 669 |
+
#transmit-btn {
|
| 670 |
+
width: 100% !important;
|
| 671 |
+
padding: 10px 16px !important;
|
| 672 |
+
font-size: 13px !important;
|
| 673 |
+
margin-bottom: 4px !important;
|
| 674 |
+
}
|
| 675 |
+
|
| 676 |
+
/* ===== NEW LAYOUT: MAIN STAGE (map | receiver) ===== */
|
| 677 |
+
#main-stage {
|
| 678 |
+
align-items: stretch !important;
|
| 679 |
+
gap: 14px !important;
|
| 680 |
+
}
|
| 681 |
+
|
| 682 |
+
#map-panel,
|
| 683 |
+
#receiver-panel {
|
| 684 |
+
background: linear-gradient(180deg, #111820 0%, #0d1219 100%) !important;
|
| 685 |
+
border: 1px solid #2a3040 !important;
|
| 686 |
+
border-radius: 12px !important;
|
| 687 |
+
padding: 14px 18px !important;
|
| 688 |
+
box-shadow: 0 4px 20px rgba(0,0,0,0.3) !important;
|
| 689 |
+
height: 100% !important;
|
| 690 |
+
display: flex !important;
|
| 691 |
+
flex-direction: column !important;
|
| 692 |
+
}
|
| 693 |
+
|
| 694 |
+
/* Children of map/receiver panels — let the right child grow to fill */
|
| 695 |
+
#map-panel > *,
|
| 696 |
+
#receiver-panel > * {
|
| 697 |
+
flex-shrink: 0;
|
| 698 |
+
}
|
| 699 |
+
|
| 700 |
+
/* The map and the monologue are the "stretch to fill" components */
|
| 701 |
+
#receiver-panel #monologue {
|
| 702 |
+
flex: 1 1 auto !important;
|
| 703 |
+
display: flex !important;
|
| 704 |
+
flex-direction: column !important;
|
| 705 |
+
justify-content: center !important;
|
| 706 |
+
min-height: 120px !important;
|
| 707 |
+
}
|
| 708 |
+
|
| 709 |
+
#receiver-panel #monologue .monologue-box {
|
| 710 |
+
max-height: none !important;
|
| 711 |
+
}
|
| 712 |
+
|
| 713 |
+
#receiver-panel #monologue .static-text {
|
| 714 |
+
flex: 1 1 auto;
|
| 715 |
+
display: flex;
|
| 716 |
+
flex-direction: column;
|
| 717 |
+
align-items: center;
|
| 718 |
+
justify-content: center;
|
| 719 |
+
}
|
| 720 |
+
|
| 721 |
+
/* The dossier accordion takes remaining space in the map column */
|
| 722 |
+
#dossier-accordion {
|
| 723 |
+
flex: 1 1 auto !important;
|
| 724 |
+
display: flex !important;
|
| 725 |
+
flex-direction: column !important;
|
| 726 |
+
min-height: 120px !important;
|
| 727 |
+
}
|
| 728 |
+
|
| 729 |
+
#dossier-accordion > .label-wrap {
|
| 730 |
+
flex-shrink: 0;
|
| 731 |
+
}
|
| 732 |
+
|
| 733 |
+
#map-panel .panel-header,
|
| 734 |
+
#receiver-panel .panel-header {
|
| 735 |
+
font-family: 'IBM Plex Mono', monospace;
|
| 736 |
+
font-size: 11px;
|
| 737 |
+
color: #c78a30;
|
| 738 |
+
letter-spacing: 3px;
|
| 739 |
+
text-transform: uppercase;
|
| 740 |
+
margin-bottom: 10px;
|
| 741 |
+
padding-bottom: 8px;
|
| 742 |
+
border-bottom: 1px solid #2a3040;
|
| 743 |
+
}
|
| 744 |
+
|
| 745 |
+
/* Constrain the map so it doesn't push the dossier off-screen */
|
| 746 |
+
#town-map {
|
| 747 |
+
max-height: 420px;
|
| 748 |
+
overflow: hidden;
|
| 749 |
+
border-radius: 8px;
|
| 750 |
+
}
|
| 751 |
+
|
| 752 |
+
#town-map .town-map-wrapper {
|
| 753 |
+
width: 100%;
|
| 754 |
+
max-height: 420px;
|
| 755 |
+
overflow: hidden;
|
| 756 |
+
}
|
| 757 |
+
|
| 758 |
+
#town-map .town-map-bg {
|
| 759 |
+
width: 100%;
|
| 760 |
+
max-height: 420px;
|
| 761 |
+
object-fit: cover;
|
| 762 |
+
object-position: center;
|
| 763 |
+
}
|
| 764 |
+
|
| 765 |
+
/* Dossier — scrolls inside its accordion so the column stays predictable */
|
| 766 |
+
#agent-profile {
|
| 767 |
+
max-height: 280px;
|
| 768 |
+
overflow-y: auto;
|
| 769 |
+
padding-right: 4px;
|
| 770 |
+
}
|
| 771 |
+
|
| 772 |
+
#dossier-accordion {
|
| 773 |
+
margin-top: 10px !important;
|
| 774 |
+
}
|
| 775 |
+
|
| 776 |
+
/* Receiver — tighter spacing so all stations fit in viewport */
|
| 777 |
+
#receiver-panel .freq-display {
|
| 778 |
+
font-size: 28px !important;
|
| 779 |
+
padding: 10px 16px !important;
|
| 780 |
+
margin: 4px 0 !important;
|
| 781 |
+
letter-spacing: 3px !important;
|
| 782 |
+
}
|
| 783 |
+
|
| 784 |
+
#receiver-panel #freq-slider {
|
| 785 |
+
margin-top: 4px !important;
|
| 786 |
+
}
|
| 787 |
+
|
| 788 |
+
#receiver-panel .monologue-box {
|
| 789 |
+
font-size: 15px !important;
|
| 790 |
+
line-height: 1.5 !important;
|
| 791 |
+
padding: 14px 16px !important;
|
| 792 |
+
min-height: 70px !important;
|
| 793 |
+
max-height: 160px !important;
|
| 794 |
+
overflow-y: auto !important;
|
| 795 |
+
margin: 10px 0 !important;
|
| 796 |
+
}
|
| 797 |
+
|
| 798 |
+
#receiver-panel .static-text {
|
| 799 |
+
padding: 14px !important;
|
| 800 |
+
font-size: 13px !important;
|
| 801 |
+
}
|
| 802 |
+
|
| 803 |
+
/* Compact emotion bars for side-by-side layout */
|
| 804 |
+
#emotions .emotion-grid {
|
| 805 |
+
grid-template-columns: 64px 1fr 32px !important;
|
| 806 |
+
gap: 4px !important;
|
| 807 |
+
font-size: 10px !important;
|
| 808 |
+
margin-bottom: 3px !important;
|
| 809 |
+
}
|
| 810 |
+
|
| 811 |
+
#emotions .emotion-bar-bg {
|
| 812 |
+
height: 6px !important;
|
| 813 |
+
}
|
| 814 |
+
|
| 815 |
+
/* Compact actions panel */
|
| 816 |
+
#actions {
|
| 817 |
+
font-size: 11px !important;
|
| 818 |
+
}
|
| 819 |
+
|
| 820 |
+
#actions > div > div {
|
| 821 |
+
margin-bottom: 5px !important;
|
| 822 |
+
line-height: 1.4 !important;
|
| 823 |
+
}
|
| 824 |
+
|
| 825 |
+
/* Bottom stage spacing */
|
| 826 |
+
#bottom-stage {
|
| 827 |
+
margin-top: 14px !important;
|
| 828 |
+
}
|
| 829 |
+
|
| 830 |
+
/* Wider viewport: keep main stage tall and aligned to fit one screen */
|
| 831 |
+
@media (min-width: 1100px) {
|
| 832 |
+
#map-panel,
|
| 833 |
+
#receiver-panel {
|
| 834 |
+
min-height: calc(100vh - 320px);
|
| 835 |
+
max-height: calc(100vh - 200px);
|
| 836 |
+
}
|
| 837 |
+
/* Map shrinks if the column is short, but never disappears */
|
| 838 |
+
#town-map,
|
| 839 |
+
#town-map .town-map-wrapper,
|
| 840 |
+
#town-map .town-map-bg {
|
| 841 |
+
max-height: calc(100vh - 620px) !important;
|
| 842 |
+
min-height: 280px !important;
|
| 843 |
+
}
|
| 844 |
+
}
|
| 845 |
+
|
| 846 |
+
/* Narrower viewport: let columns wrap naturally and remove forced heights */
|
| 847 |
+
@media (max-width: 1099px) {
|
| 848 |
+
#main-stage {
|
| 849 |
+
flex-wrap: wrap !important;
|
| 850 |
+
}
|
| 851 |
+
#map-panel,
|
| 852 |
+
#receiver-panel {
|
| 853 |
+
min-height: auto !important;
|
| 854 |
+
}
|
| 855 |
+
#town-map,
|
| 856 |
+
#town-map .town-map-wrapper,
|
| 857 |
+
#town-map .town-map-bg {
|
| 858 |
+
max-height: 320px !important;
|
| 859 |
+
}
|
| 860 |
+
}
|
| 861 |
"""
|
tin_lantern_junction_map.png
ADDED
|
Git LFS Details
|
verdant_spire_map.png
ADDED
|
Git LFS Details
|