| """Interactive "step inside" exploration — click a place, meet who's there. |
| |
| Built on Gradio's native Gallery component (reliable, image-rich, clickable) |
| plus a couple of HTML panels, so exploration feels spatial instead of |
| dropdown-driven. |
| """ |
|
|
| from __future__ import annotations |
|
|
| import random |
|
|
| from ui import assets |
| from ui.entity_card import render_entity_card |
| from world.entities import get_entities_by_location |
| from world.locations import get_all_locations, get_location_by_id |
|
|
| _AMBIENCE = { |
| "library": "Dust hangs in the candlelight. Somewhere a quill is still writing.", |
| "sea": "Cold fog rolls in. A voice you almost recognise calls your name.", |
| "clock-forest": "Everything ticks slightly out of time. The leaves fall upward.", |
| "moon-market": "A hundred unnamed colours of lantern-light. Smoke that smells sweet.", |
| "valley": "The ground rises and falls — slow, enormous, breathing.", |
| "crossroads": "The bonfire shifts colour as you approach. It is deciding about you.", |
| "mirror-bogs": "The water is too still. Your reflection arrives a moment late.", |
| "hollow-mountain": "Cold stone, older light. Echoes of rooms that are elsewhere.", |
| } |
|
|
|
|
| def place_gallery_items() -> list: |
| """(image_url, caption) tuples for the 8 places, in stable order. |
| |
| We pass the served Gradio file URL (not a filesystem path) so that |
| dynamic gallery updates work across Gradio 5.5+/6 without tripping the |
| stricter local-file cache check. |
| """ |
| items = [] |
| for loc in get_all_locations(): |
| name = assets.LOCATION_IMAGES.get(loc["slug"]) |
| count = loc.get("entity_count", 0) |
| caption = f"{loc['name'].replace('The ', '')} · {count} here" |
| if name: |
| items.append((str(assets.ASSET_DIR / name), caption)) |
| return items |
|
|
|
|
| def place_ids() -> list[int]: |
| return [loc["id"] for loc in get_all_locations() if assets.LOCATION_IMAGES.get(loc["slug"])] |
|
|
|
|
| def _soul_tile_image(entity: dict, slug: str | None) -> str | None: |
| """Bespoke cutout portrait for a soul (named art, else type fallback).""" |
| return assets.sprite_path(entity) |
|
|
|
|
| def soul_gallery(location_id: int) -> tuple[list, list[str]]: |
| """Return (gallery_items, ordered_entity_ids) for everyone in a place.""" |
| loc = get_location_by_id(location_id) |
| slug = loc["slug"] if loc else None |
| entities = get_entities_by_location(location_id, limit=24) |
| items, ids = [], [] |
| for e in entities: |
| img = _soul_tile_image(e, slug) |
| if not img: |
| continue |
| label = e["display_name"] |
| if e["status"] == "legendary": |
| label = "★ " + label |
| items.append((img, label)) |
| ids.append(e["id"]) |
| return items, ids |
|
|
|
|
| def render_room(location_id: int | None) -> str: |
| if not location_id: |
| return """ |
| <div class="room-empty"> |
| <p class="room-empty-title">Step into the world</p> |
| <p class="room-empty-text">Choose a place above. You'll walk inside it, feel its air, |
| and meet the souls who live there. Click any soul to truly meet them.</p> |
| </div> |
| """ |
| loc = get_location_by_id(location_id) |
| if not loc: |
| return "" |
| img = assets.location_image_url(loc["slug"]) |
| bg = f"background-image: url('{img}');" if img else "" |
| ambience = _AMBIENCE.get(loc["slug"], loc.get("aesthetic_description", "")) |
| count = loc.get("entity_count", 0) |
| vibes = "".join(f'<span class="loc-vibe">{v}</span>' for v in loc.get("vibe_tags", [])[:4]) |
|
|
| return f""" |
| <div class="room-view"> |
| <div class="room-hero" style="{bg}"> |
| <div class="room-hero-scrim"></div> |
| <div class="room-hero-text"> |
| <p class="room-eyebrow">You step inside</p> |
| <h2 class="room-name">{loc['name']}</h2> |
| <p class="room-ambience">{ambience}</p> |
| </div> |
| </div> |
| <div class="room-body"> |
| <div class="loc-card-vibes">{vibes}</div> |
| <p class="room-lore">{loc['full_lore']}</p> |
| <p class="room-special">✦ {loc.get('special_property', '')}</p> |
| <p class="room-count">{count} {'soul dwells' if count == 1 else 'souls dwell'} here — click a face to meet them</p> |
| </div> |
| </div> |
| """ |
|
|
|
|
| def render_soul(entity_id: str | None, entity_ids: list[str] | None): |
| from world.entities import get_entity |
|
|
| if not entity_id: |
| return '<div class="feed-empty">Click a soul above to meet them.</div>' |
| entity = get_entity(entity_id) |
| if not entity: |
| return '<div class="feed-empty">They have wandered off.</div>' |
| return render_entity_card(entity) |
|
|
|
|
| def random_place_id() -> int: |
| ids = place_ids() |
| return random.choice(ids) if ids else 0 |
|
|