Spaces:
Running
Running
| """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 | |