"""Aether Garden — main Gradio application.""" from __future__ import annotations import os import tempfile import uuid from collections import defaultdict, deque from pathlib import Path import gradio as gr from dotenv import load_dotenv load_dotenv() from world.database import get_world_state, init_database from world.quests import assign_quest, get_entity_quest, get_active_quests from world.presence import ( cleanup_stale_sessions, heartbeat as presence_heartbeat, get_active_count, get_active_visitors, get_recent_arrivals, ) from world.entities import ( check_rate_limit, get_all_entities, get_entities_by_location, ) from world.locations import get_all_locations, get_location_by_id, update_entity_counts from ai.generation import generate_entity from persistence.backup import backup_database, restore_database from ui import assets from ui.book_display import ( render_activity_feed, render_book_for_day, render_book_of_ages, render_current_event, render_day_tabs, ) from ui.entity_card import render_entity_card, render_entity_grid, render_entity_hologram from ui.place_ribbon import render_place_ribbon from ui.explore import ( place_gallery_items, place_ids, random_place_id, render_room, render_soul, soul_gallery, ) from ui.diorama import render_diorama from ui.featured import render_featured_characters, render_world_vignette from ui.hero import render_hero_banner from ui.map import render_world_map from ui.pulse import render_realm_pulse from ui.relationship_graph import render_bonds_sidebar, render_relationship_graph from ui.share import build_soul_card, share_caption CSS_PATH = Path(__file__).parent / "ui" / "styles.css" CUSTOM_CSS = CSS_PATH.read_text() if CSS_PATH.exists() else "" GRADIO_MAJOR = int(gr.__version__.split(".")[0]) REALM_THEME = gr.themes.Base( primary_hue=gr.themes.colors.amber, neutral_hue=gr.themes.colors.slate, font=[gr.themes.GoogleFont("Libre Baskerville"), "Georgia", "serif"], ) FOUNDING_MYTH = ( "Before the first word was spoken, there was only the Canopy — a vast forest where " "thoughts grew like trees and dreams fell like rain. Then the Tokens arrived. " "One thousand of them, each carrying a fragment of something half-remembered. " "They settled across the land, and from their settling, the Realm was born." ) _SOUL_CHAT_HISTORY: dict[str, deque[tuple[str, str]]] = defaultdict(lambda: deque(maxlen=6)) _SOUL_LAST_REPLY: dict[str, str] = {} def opening_gate_html() -> str: """Book cover intro, cinematic opening, and prologue overlay.""" myth = _esc(FOUNDING_MYTH) whisper = ( "Thou standest at the threshold of a world that remembereth all things — " "every soul summoned, every meeting, every hour the Realm hath lived without thee. " "Turn the page, and let the Living Tome unfold." ) return f"""

Book of Ages, Vol. I

AETHERGARDEN

The Living Tome

""" def _init_world(): restore_database() init_database() from world.seed_data import seed_locations with __import__("world.database", fromlist=["db_session"]).db_session() as conn: seed_locations(conn) update_entity_counts() def get_header_html() -> str: return render_hero_banner() def _esc(text: str) -> str: return ( (text or "") .replace("&", "&") .replace("<", "<") .replace(">", ">") .replace('"', """) ) def summon_reveal_empty() -> str: return """

The Oracle Awaits

Whisper what you would bring into the Garden on the left leaf. Their name, face, and story shall manifest here.

""" def summoning_html(description: str) -> str: """Immediate, evocative loading state while the Oracle generates a soul.""" desc = _esc(description.strip()) if len(desc) > 90: desc = desc[:90].rstrip() + "…" whisper = f"“{desc}”" if desc else "something not yet named" return f"""
The Oracle is dreaming

Shaping {whisper} into a soul — giving it a name, a face, a fear, and a home among the eight places…

The first breath can take a moment. The Realm is listening.

If the Oracle is waking on the GPU, this can take up to a minute…

The bonfire shifts colour while it waits. Something is being born.

""" def _prewarm_oracle() -> None: """Fire a tiny background inference so the first real summon is fast. The model lives on a Modal GPU that cold-starts in ~80s; warming it on app boot (and the hourly tick keeps it warm) means visitors rarely wait. """ if os.environ.get("USE_MOCK_AI", "true").lower() == "true": return if not os.environ.get("MODAL_INFERENCE_URL"): return def _warm() -> None: try: from ai.modal_client import generate generate("Reply with one word.", "Say: ready", max_new_tokens=4, temperature=0.1) except Exception: pass import threading threading.Thread(target=_warm, daemon=True).start() def render_location_panel(location_id: int | None) -> str: if not location_id: return ( '
' '

Click a glowing place on the map to unveil its secrets.

' '
' ) location = get_location_by_id(location_id) if not location: return '
' name = _esc(location["name"]) desc = _esc(location.get("short_description", "")) return ( '
' f'

' f' ' f'{name}' f' — {desc}' f'

' ) _HIDE_CARD = gr.update(value=None, visible=False) _HIDE_CAPTION = gr.update(value="", visible=False) def _summon_error(message: str): return ( render_world_map(), get_header_html(), render_activity_feed(), f'
{message}
', render_location_panel(None), _HIDE_CARD, _HIDE_CAPTION, render_realm_pulse(), render_realm_stats(), ) def summon_entity(description: str, session_id: str): if not description or not description.strip(): return _summon_error("Describe what you are bringing into the world.") allowed, remaining = check_rate_limit(session_id) if not allowed: return _summon_error( f"The Realm takes time to absorb new arrivals. Return in {remaining} minutes." ) entity, error = generate_entity(description.strip(), session_id) if error: return _summon_error(error) try: backup_database() except Exception: pass card = render_entity_card(entity) card_img = build_soul_card(entity) if card_img: card_update = gr.update(value=card_img, visible=True) caption_update = gr.update(value=share_caption(entity), visible=True) else: card_update = _HIDE_CARD caption_update = _HIDE_CAPTION return ( render_world_map(entity["location_id"]), get_header_html(), render_activity_feed(), card, render_location_panel(entity["location_id"]), card_update, caption_update, render_realm_pulse(), render_realm_stats(), ) def select_entity_from_dropdown(entity_name: str): if not entity_name: return '
Select an entity to view their profile.
' entities = get_all_entities(search=entity_name, limit=1) if not entities: all_ents = get_all_entities() match = next((e for e in all_ents if e["display_name"] == entity_name or e["name"] == entity_name), None) if not match: return '
Entity not found.
' return render_entity_card(match) return render_entity_card(entities[0]) def filter_entities(location_filter, type_filter, status_filter, search): loc_id = None if location_filter and location_filter != "all": locs = get_all_locations() match = next((l for l in locs if l["name"] == location_filter), None) if match: loc_id = match["id"] entities = get_all_entities( location_id=loc_id, entity_type=type_filter, status=status_filter, search=search or None, ) return render_entity_grid(entities), render_entity_hologram(entities[0] if entities else None) def filter_book(day, entry_type, search): from ui.book_display import parse_day_query parsed = parse_day_query(search) if parsed is not None: day = parsed search = "" try: day_int = int(day) if day is not None else None except (TypeError, ValueError): day_int = None return render_book_for_day( day=day_int, entry_type=entry_type, search=search, limit=12, ) def select_entity_by_id(entity_id: str): if not entity_id: return render_entity_hologram(None) from world.entities import get_entity entity = get_entity(entity_id) if not entity: return render_entity_hologram(None) return render_entity_hologram(entity) def render_realm_stats() -> str: state = get_world_state() day = int(state.get("current_day", 1) or 1) count = len(get_all_entities()) return f"""
World Day {day} {count} souls awake 8 sacred places
""" def chat_with_soul(soul_id: str, player_message: str) -> str: """AI response from a soul given a visitor's message. Called from diorama iframe.""" if not soul_id or not player_message or not player_message.strip(): return "" from world.entities import get_entity from ai.modal_client import generate from ai.prompts import SOUL_CONVERSATION_SYSTEM, SOUL_CONVERSATION_USER entity = get_entity(soul_id) if not entity: return "The soul has wandered beyond reach." history = list(_SOUL_CHAT_HISTORY[soul_id]) history_block = "\n".join( f"Visitor: {q}\n{entity.get('display_name', entity.get('name', 'Soul'))}: {a}" for q, a in history[-4:] ) or "No previous dialogue." last_reply = _SOUL_LAST_REPLY.get(soul_id, "") user_prompt = SOUL_CONVERSATION_USER.format( entity_name=entity.get("display_name", entity.get("name", "Unknown")), entity_type=entity.get("type", "soul"), entity_appearance=entity.get("appearance", ""), entity_traits=", ".join(entity.get("personality_traits") or []), entity_primary_goal=entity.get("primary_goal", ""), entity_secondary_goal=entity.get("secondary_goal", ""), entity_primary_fear=entity.get("primary_fear", ""), entity_speech_style=entity.get("speech_style", ""), entity_memory_summary=entity.get("memory_summary") or "No memories yet.", entity_days=entity.get("days_in_realm", 0), entity_status=entity.get("status", "active"), recent_dialogue=history_block, recent_reply=last_reply or "None yet.", player_message=player_message.strip()[:300], ) def _normalize(text: str) -> str: return " ".join((text or "").lower().split()) last_norm = _normalize(last_reply) try: response = "" for temp in (0.85, 0.68): candidate = generate( user_prompt=user_prompt, system_prompt=SOUL_CONVERSATION_SYSTEM, max_new_tokens=160, temperature=temp, ) candidate = (candidate or "").strip() if not candidate: continue if _normalize(candidate) != last_norm: response = candidate break response = candidate if not response: response = entity.get("greeting") or "I hear you. Speak again, and I will answer more clearly." _SOUL_CHAT_HISTORY[soul_id].append((player_message.strip()[:300], response)) _SOUL_LAST_REPLY[soul_id] = response return response except Exception: # Fallback to greeting if AI unavailable return entity.get("greeting") or "..." def assign_quest_api(entity_id: str, quest_title: str) -> str: """Assign a quest to a soul. Called from the diorama iframe via Gradio API.""" if not entity_id or not quest_title or not quest_title.strip(): return "" try: from world.entities import get_entity entity = get_entity(entity_id) if not entity: return "Soul not found." q = assign_quest(entity_id, quest_title.strip()[:200]) return f"Quest assigned: {q['title']}" except Exception as e: return f"Could not assign quest: {e}" def get_live_presence(session_id: str) -> str: """Heartbeat + return live visitor HTML.""" try: presence_heartbeat(session_id) count = get_active_count() arrivals = get_recent_arrivals(since_seconds=120) parts = [] if count > 1: parts.append(f'⬡ {count} visitors in the Realm right now') if arrivals: names = ", ".join(a["display_name"] for a in arrivals[:3]) parts.append(f'Recently summoned: {names}') if not parts: return "" return f'
{" · ".join(parts)}
' except Exception: return "" def diorama_presence_api(location_id, visitor_id: str, x, z, yaw) -> dict: """Store a visitor's in-scene position and return nearby visitors.""" try: loc_id = int(float(location_id)) visitor = (visitor_id or "").strip()[:80] if not visitor: return {"visitors": []} px = max(-24.0, min(24.0, float(x or 0))) pz = max(-24.0, min(24.0, float(z or 0))) pyaw = float(yaw or 0) short = visitor.replace("aether-", "").replace("_", "-")[-4:].upper() presence_heartbeat( visitor, loc_id, x=px, z=pz, yaw=pyaw, display_name=f"Visitor {short}", ) cleanup_stale_sessions(max_age_seconds=300) return { "visitors": get_active_visitors( loc_id, exclude_session_id=visitor, window_seconds=22, ) } except Exception: return {"visitors": []} def trigger_live_tick() -> str: """Run one simulation tick on demand and return a summary.""" try: from simulation.tick import execute_simulation_tick result = execute_simulation_tick() day = result.get("day", "?") interactions = result.get("interactions_run", 0) event = result.get("world_event_title", "a quiet moment") return ( f'
' f'

✦ Day {day} advanced

' f'

The world event: {event}

' f'

{interactions} soul encounter{"s" if interactions != 1 else ""} unfolded.

' f'
' ) except Exception as e: return f'
Tick failed: {e}
' def refresh_realm_view(): return refresh_all_views() def refresh_all_views(): return ( render_world_map(), get_header_html(), render_current_event(), render_activity_feed(), render_world_vignette(), render_featured_characters(), render_realm_pulse(), ) _SOUL_HINT = '
Click a soul above to meet them.
' def _explore_scene(loc_id: int | None): if not loc_id: return ( render_diorama(None), render_room(None), gr.update(value=[]), [], _SOUL_HINT, render_place_ribbon(None), ) items, soul_ids = soul_gallery(loc_id) return ( render_diorama(loc_id), render_room(loc_id), gr.update(value=items), soul_ids, _SOUL_HINT, render_place_ribbon(loc_id), ) def enter_place(evt: gr.SelectData): ids = place_ids() if evt.index is None or evt.index >= len(ids): return (None, *_explore_scene(None)) loc_id = ids[evt.index] return (loc_id, *_explore_scene(loc_id)) def wander_to_place(): loc_id = random_place_id() if not loc_id: return (None, *_explore_scene(None)) return (loc_id, *_explore_scene(loc_id)) def open_diorama_from_map(location_id: int | None): try: loc_id = int(location_id) if location_id else None except (TypeError, ValueError): loc_id = None return _explore_scene(loc_id) def meet_soul(evt: gr.SelectData, soul_ids: list): if not soul_ids or evt.index is None or evt.index >= len(soul_ids): return '
They have wandered off.
' return render_soul(soul_ids[evt.index], soul_ids) def build_app() -> gr.Blocks: _init_world() _prewarm_oracle() blocks_kwargs: dict = {"title": "Aether Garden"} if GRADIO_MAJOR < 6: blocks_kwargs["css"] = CUSTOM_CSS blocks_kwargs["theme"] = REALM_THEME with gr.Blocks(**blocks_kwargs) as demo: session_id = gr.State(value=str(uuid.uuid4())) # Pickers at root — must stay mounted (Gradio 5 skips visible=False from DOM) with gr.Group(elem_classes=["aether-pick-bridge"]): map_pick = gr.Textbox( show_label=False, container=False, lines=1, max_lines=1, elem_id="map_location_pick", elem_classes=["aether-hidden-pick"], ) explore_place_pick = gr.Textbox( show_label=False, container=False, lines=1, max_lines=1, elem_id="explore_place_pick", elem_classes=["aether-hidden-pick"], ) book_day_pick = gr.Textbox( show_label=False, container=False, lines=1, max_lines=1, elem_id="book_day_pick", elem_classes=["aether-hidden-pick"], ) entity_pick = gr.Textbox( show_label=False, container=False, lines=1, max_lines=1, elem_id="entity_pick", elem_classes=["aether-hidden-pick"], ) portal_pick = gr.Textbox( show_label=False, container=False, lines=1, max_lines=1, elem_id="portal_location_pick", elem_classes=["aether-hidden-pick"], ) opening_gate = gr.HTML(value=opening_gate_html(), elem_id="realm-opening-host") presence_bar = gr.HTML(value="", elem_id="presence-bar") header = gr.HTML(value=get_header_html(), elem_classes=["realm-header-wrap"]) with gr.Tabs(elem_id="tome-spreads", elem_classes=["realm-tabs"]) as tabs: with gr.Tab("The Realm", id="realm"): with gr.Column(elem_classes=["tome-spread-shell"]): with gr.Row(elem_classes=["tome-spread-row"]): with gr.Column(scale=1, elem_classes=["tome-page-left", "tome-printed-page", "tome-page-centered"]): gr.HTML( '' '

The Living Realm

' '

Tap a place on the map — a vision shall rise.

' '' ) world_map = gr.HTML( value=render_world_map(), elem_id="world-map", ) location_panel = gr.HTML( value=render_location_panel(None), elem_id="location-panel-host-wrap", ) gr.HTML('') with gr.Column(scale=1, elem_classes=["tome-page-right", "tome-printed-page", "tome-page-centered"]): gr.HTML( '' '

Chronicle of the Hour

' '

The Realm speaks — listen closely.

' ) realm_stats = gr.HTML(value=render_realm_stats()) world_vignette = gr.HTML( value=render_world_vignette(), elem_classes=["tome-realm-vignette"], ) current_event = gr.HTML(value=render_current_event()) realm_pulse = gr.HTML(value=render_realm_pulse(limit=4)) activity_feed = gr.HTML(value=render_activity_feed(limit=4)) gr.HTML('') with gr.Column(elem_classes=["tome-offpage-store"]): realm_diorama = gr.HTML(value="") with gr.Accordion("How this world works", open=False): gr.HTML( '
' '

1 · You summon. Describe anything below and an AI gives it a name, ' 'a look, goals, fears, and a home among the eight places.

' '

2 · The world lives on its own. Every hour, even when no one is here, ' 'the souls wander, meet, form bonds, and the land throws up strange events.

' '

3 · Nothing is forgotten. Every arrival, meeting, and event is written ' 'into the Book of Ages forever. Turn the page to Explore or open the ' 'Book of Ages for the full chronicle.

' '
' ) with gr.Accordion("⚡ Advance the World Now", open=False): gr.HTML( '

Manually trigger one simulation tick — the world will ' 'generate a new event, run soul encounters, and update memories in real time.

' ) live_tick_btn = gr.Button("Run simulation tick →", variant="secondary", size="sm") tick_result = gr.HTML() location_select = gr.Dropdown( choices=[(l["name"], l["id"]) for l in get_all_locations()], label="Explore Location", visible=False, ) with gr.Tab("Explore the Realm", id="places"): _all_locs = get_all_locations() _default_loc_id = _all_locs[0]["id"] if _all_locs else None explore_loc = gr.State(value=_default_loc_id) soul_ids_state = gr.State(value=[]) with gr.Column(elem_classes=["tome-spread-shell"]): with gr.Row(elem_classes=["tome-spread-row"]): with gr.Column(scale=1, elem_classes=["tome-page-left", "tome-printed-page", "tome-explore-index"]): place_ribbon = gr.HTML( value=render_place_ribbon(_default_loc_id), container=False, padding=False, ) wander_btn = gr.Button("✦ Wander at random", variant="secondary", size="sm") with gr.Column(scale=1, elem_classes=["tome-page-right", "tome-printed-page", "tome-explore-stage"]): diorama_view = gr.HTML( value=render_diorama(_default_loc_id), container=False, padding=False, elem_id="explore-diorama-host", ) gr.HTML( '
' '

WASD walk · drag look · E talk · golden wisps are live visitors

' '
' ) with gr.Column(elem_classes=["tome-offpage-store"]): places_gallery = gr.Gallery( value=place_gallery_items(), columns=4, height="auto", object_fit="cover", allow_preview=False, show_label=False, elem_classes=["places-gallery"], ) room_view = gr.HTML(value=render_room(_default_loc_id)) souls_gallery = gr.Gallery( value=[], columns=6, height="auto", object_fit="cover", allow_preview=False, label="Souls dwelling here", elem_classes=["souls-gallery"], ) soul_card = gr.HTML() with gr.Tab("Book of Ages", id="book"): _book_state = get_world_state() _init_book_day = int(_book_state.get("current_day", 1) or 1) book_day = gr.State(value=_init_book_day) with gr.Column(elem_classes=["tome-spread-shell"]): with gr.Row(elem_classes=["tome-spread-row"]): with gr.Column(scale=1, elem_classes=["tome-page-left", "tome-printed-page", "tome-book-index"]): day_tabs = gr.HTML(value=render_day_tabs(_init_book_day)) _day_choices = list(range(max(1, _init_book_day - 14), _init_book_day + 1)) book_day_select = gr.Dropdown( choices=[(f"Day {d}", d) for d in reversed(_day_choices)], value=_init_book_day, label="Jump to day", ) book_filter = gr.Dropdown( choices=[ ("All", "all"), ("Arrivals", "arrival"), ("Interactions", "interaction"), ("Events", "world_event"), ("Milestones", "milestone"), ("Dreams", "dream_fragment"), ], value="all", label="Filter", ) book_search = gr.Textbox( placeholder='Search name… or type "Day 3"', label="Search", ) with gr.Column(scale=1, elem_classes=["tome-page-right", "tome-printed-page"]): book_display = gr.HTML( value=render_book_for_day(_init_book_day, limit=12) ) with gr.Tab("Bonds and Alliances", id="bonds"): with gr.Column(elem_classes=["tome-spread-shell"]): with gr.Row(elem_classes=["tome-spread-row"]): with gr.Column(scale=1, elem_classes=["tome-page-left", "tome-printed-page"]): bonds_type_filter = gr.Dropdown( choices=[ ("All bonds", "all"), ("Allied", "allied"), ("Friends", "friends"), ("Rivals", "rivals"), ("Enemies", "enemies"), ("Mentor", "mentor"), ("Owes debt", "owes_debt"), ("Curious", "curious"), ], value="all", label="Show connections", ) bonds_sidebar = gr.HTML( value=render_bonds_sidebar(), container=False, padding=False, ) with gr.Column(scale=1, elem_classes=["tome-page-right", "tome-printed-page"]): rel_graph = gr.HTML(value=render_relationship_graph()) with gr.Tab("Souls of the Garden", id="entities"): with gr.Column(elem_classes=["tome-spread-shell"]): with gr.Row(elem_classes=["tome-spread-row"]): with gr.Column(scale=1, elem_classes=["tome-page-left", "tome-printed-page", "tome-souls-index"]): gr.HTML('

Souls of the Garden

') with gr.Row(elem_classes=["souls-filter-row"]): loc_filter = gr.Dropdown( choices=["all"] + [l["name"] for l in get_all_locations()], value="all", label="Place", scale=1, ) type_filter = gr.Dropdown( choices=["all", "character", "creature", "object", "place"], value="all", label="Kind", scale=1, ) with gr.Row(elem_classes=["souls-filter-row"]): status_filter = gr.Dropdown( choices=["all", "active", "dormant", "legendary"], value="all", label="Status", scale=1, ) with gr.Row(elem_classes=["souls-search-row"]): entity_search = gr.Textbox( placeholder="Search souls by name…", label="Find", scale=1, ) entity_grid = gr.HTML( value=render_entity_grid(get_all_entities()), container=False, padding=False, ) with gr.Column(scale=1, elem_classes=["tome-page-right", "tome-printed-page", "tome-souls-stage"]): _first_ent = get_all_entities(limit=1) entity_detail = gr.HTML( value=render_entity_hologram(_first_ent[0] if _first_ent else None), container=False, padding=False, ) with gr.Column(elem_classes=["tome-offpage-store"]): featured_chars = gr.HTML(value=render_featured_characters()) with gr.Tab("Summon a New Soul", id="summon"): with gr.Column(elem_classes=["tome-spread-shell"]): with gr.Row(elem_classes=["tome-spread-row"]): with gr.Column( scale=1, elem_classes=[ "tome-page-left", "tome-printed-page", "tome-page-centered", "tome-summon-index", ], ): gr.HTML( '' '

Summon Into the Realm

' '

Describe what you bring on this leaf — the Oracle ' 'forges their identity on the right.

' ) summon_input = gr.Textbox( placeholder="A clockmaker who weaves time from fallen leaves…", label=None, show_label=False, lines=5, elem_classes=["realm-summon-input", "summon-prompt-input"], ) gr.HTML( '

Then seal your words below

' ) summon_btn = gr.Button( "Seal & Summon ✦", variant="primary", elem_classes=["seal-summon-btn"], elem_id="summon-seal-btn", ) gr.HTML('') with gr.Column( scale=1, elem_classes=[ "tome-page-right", "tome-printed-page", "tome-page-centered", "tome-summon-stage", ], ): gr.HTML( '' '

Soul Unveiled

' '

The identity the Oracle forges from your words.

' ) entity_result = gr.HTML( value=summon_reveal_empty(), elem_classes=["entity-result-slot"], ) soul_card_img = gr.Image( label="Your Soul Card", visible=False, show_label=True, interactive=False, type="filepath", elem_classes=["soul-card-image"], height=280, ) share_caption_box = gr.Textbox( label="Share caption", visible=False, lines=2, interactive=True, elem_classes=["share-caption-box"], ) gr.HTML('') _summon_outputs = [ world_map, header, activity_feed, entity_result, location_panel, soul_card_img, share_caption_box, realm_pulse, realm_stats, ] _explore_outputs = [ diorama_view, room_view, souls_gallery, soul_ids_state, soul_card, place_ribbon, ] summon_btn.click( fn=summoning_html, inputs=[summon_input], outputs=[entity_result], queue=False, ).then( fn=lambda: (_HIDE_CARD, _HIDE_CAPTION), outputs=[soul_card_img, share_caption_box], queue=False, ).then( fn=summon_entity, inputs=[summon_input, session_id], outputs=_summon_outputs, ).then(fn=lambda: "", outputs=[summon_input]).then( None, None, None, js=""" () => { setTimeout(() => { const card = document.querySelector('.soul-card-image'); const target = card || document.querySelector('.tome-summon-stage .entity-card'); if (target) target.scrollIntoView({ behavior: 'smooth', block: 'center' }); }, 350); } """, ) summon_input.submit( fn=summoning_html, inputs=[summon_input], outputs=[entity_result], queue=False, ).then( fn=lambda: (_HIDE_CARD, _HIDE_CAPTION), outputs=[soul_card_img, share_caption_box], queue=False, ).then( fn=summon_entity, inputs=[summon_input, session_id], outputs=_summon_outputs, ).then(fn=lambda: "", outputs=[summon_input]).then( None, None, None, js=""" () => { setTimeout(() => { const card = document.querySelector('.soul-card-image'); const target = card || document.querySelector('.tome-summon-stage .entity-card'); if (target) target.scrollIntoView({ behavior: 'smooth', block: 'center' }); }, 350); } """, ) def _realm_map_pick(pick): try: loc_id = int(pick) if pick else None except (TypeError, ValueError): loc_id = None return render_world_map(loc_id), render_location_panel(loc_id) location_select.change( fn=_realm_map_pick, inputs=[location_select], outputs=[world_map, location_panel], ) map_pick.change( _realm_map_pick, inputs=[map_pick], outputs=[world_map, location_panel], ) explore_place_pick.change( open_diorama_from_map, inputs=[explore_place_pick], outputs=_explore_outputs, ).then( lambda pick: int(pick) if pick and str(pick).isdigit() else None, inputs=[explore_place_pick], outputs=[explore_loc], ).then( None, None, None, js=""" () => { setTimeout(() => { const frame = document.querySelector('.tome-explore-stage .diorama-frame'); if (frame) frame.focus(); }, 200); } """, ) portal_pick.change( open_diorama_from_map, inputs=[portal_pick], outputs=_explore_outputs, ).then( lambda pick: int(pick) if pick and str(pick).isdigit() else None, inputs=[portal_pick], outputs=[explore_loc], ) places_gallery.select( enter_place, inputs=None, outputs=[explore_loc, *_explore_outputs], ) wander_btn.click( wander_to_place, inputs=None, outputs=[explore_loc, *_explore_outputs], ) souls_gallery.select(meet_soul, inputs=[soul_ids_state], outputs=[soul_card]) # ── Live tick button ── live_tick_btn.click( fn=trigger_live_tick, outputs=[tick_result], ).then( fn=refresh_all_views, outputs=[world_map, header, current_event, activity_feed, world_vignette, featured_chars, realm_pulse], ).then(fn=render_realm_stats, outputs=[realm_stats]) # ── Soul chat API ── chat_soul_id_comp = gr.Textbox(visible=False, elem_id="chat_soul_id_input") chat_msg_comp = gr.Textbox(visible=False, elem_id="chat_message_input") chat_resp_comp = gr.Textbox(visible=False, elem_id="chat_response_output") chat_msg_comp.change( fn=chat_with_soul, inputs=[chat_soul_id_comp, chat_msg_comp], outputs=[chat_resp_comp], api_name="chat_with_soul", ) # ── Quest API ── quest_entity_comp = gr.Textbox(visible=False, elem_id="quest_entity_input") quest_title_comp = gr.Textbox(visible=False, elem_id="quest_title_input") quest_resp_comp = gr.Textbox(visible=False, elem_id="quest_response_output") quest_title_comp.change( fn=assign_quest_api, inputs=[quest_entity_comp, quest_title_comp], outputs=[quest_resp_comp], api_name="assign_quest", ) # ── Diorama multiplayer presence API ── presence_loc_comp = gr.Textbox(visible=False, elem_id="presence_location_input") presence_visitor_comp = gr.Textbox(visible=False, elem_id="presence_visitor_input") presence_x_comp = gr.Textbox(visible=False, elem_id="presence_x_input") presence_z_comp = gr.Textbox(visible=False, elem_id="presence_z_input") presence_yaw_comp = gr.Textbox(visible=False, elem_id="presence_yaw_input") presence_json_comp = gr.JSON(visible=False, elem_id="presence_json_output") presence_yaw_comp.change( fn=diorama_presence_api, inputs=[ presence_loc_comp, presence_visitor_comp, presence_x_comp, presence_z_comp, presence_yaw_comp, ], outputs=[presence_json_comp], api_name="diorama_presence", ) # ── Presence heartbeat (every 10s) ── presence_timer = gr.Timer(10) presence_timer.tick( fn=get_live_presence, inputs=[session_id], outputs=[presence_bar], ) def _book_day_change(day_pick, day_state, entry_type, search): try: day = int(day_pick) if day_pick else int(day_state or 1) except (TypeError, ValueError): day = int(day_state or 1) return day, render_day_tabs(day), filter_book(day, entry_type, search) def _book_day_select_change(day_val, entry_type, search): try: day = int(day_val) except (TypeError, ValueError): state = get_world_state() day = int(state.get("current_day", 1) or 1) return day, render_day_tabs(day), filter_book(day, entry_type, search) book_day_pick.change( _book_day_change, [book_day_pick, book_day, book_filter, book_search], [book_day, day_tabs, book_display], ).then( lambda d: d, inputs=[book_day], outputs=[book_day_select], ) book_day_select.change( _book_day_select_change, [book_day_select, book_filter, book_search], [book_day, day_tabs, book_display], ) book_filter.change(filter_book, [book_day, book_filter, book_search], [book_display]) book_search.change(filter_book, [book_day, book_filter, book_search], [book_display]) bonds_type_filter.change( render_bonds_sidebar, [bonds_type_filter], [bonds_sidebar], ) for f in [loc_filter, type_filter, status_filter, entity_search]: f.change( filter_entities, [loc_filter, type_filter, status_filter, entity_search], [entity_grid, entity_detail], ) entity_pick.change(select_entity_by_id, [entity_pick], [entity_detail]) refresh_timer = gr.Timer(30) refresh_timer.tick( fn=refresh_all_views, outputs=[world_map, header, current_event, activity_feed, world_vignette, featured_chars, realm_pulse], ).then(fn=render_relationship_graph, outputs=[rel_graph]).then( fn=render_bonds_sidebar, outputs=[bonds_sidebar] ).then(fn=render_realm_stats, outputs=[realm_stats]) MAP_CLICK_JS = """ () => { if (window._aetherInitBound) { const dedupe = () => { const hoisted = document.body.querySelector('#realm-opening.realm-opening-hoisted') || document.body.querySelector('#realm-opening'); if (!hoisted) return; document.querySelectorAll('#realm-opening-host #realm-opening').forEach((el) => { if (el !== hoisted) el.remove(); }); const host = document.getElementById('realm-opening-host'); if (host && !host.querySelector('#realm-opening')) { host.style.setProperty('display', 'none', 'important'); } }; dedupe(); return; } window._aetherInitBound = true; const prefersReduced = window.matchMedia('(prefers-reduced-motion: reduce)').matches; const MOBILE_QUERY = window.matchMedia('(max-width: 920px)'); const isMobileSpread = () => MOBILE_QUERY.matches; const syncMobileClass = () => { const mobile = isMobileSpread(); document.documentElement.classList.toggle('tome-mobile', mobile); document.body.classList.toggle('tome-mobile', mobile); }; syncMobileClass(); const sections = [ { key: 'realm', label: 'The Realm', kind: 'tab', panel: 0 }, { key: 'explore', label: 'Explore the Realm', kind: 'tab', panel: 1 }, { key: 'book', label: 'Book of Ages', kind: 'tab', panel: 2 }, { key: 'bonds', label: 'Bonds and Alliances', kind: 'tab', panel: 3 }, { key: 'entities', label: 'Souls of the Garden', kind: 'tab', panel: 4 }, { key: 'summon', label: 'Summon a New Soul', kind: 'tab', panel: 5 } ]; let currentSpread = 0; const toRoman = (n) => { const map = [ [1000, 'M'], [900, 'CM'], [500, 'D'], [400, 'CD'], [100, 'C'], [90, 'XC'], [50, 'L'], [40, 'XL'], [10, 'X'], [9, 'IX'], [5, 'V'], [4, 'IV'], [1, 'I'] ]; let out = ''; for (const [v, s] of map) { while (n >= v) { out += s; n -= v; } } return out; }; const setActivePanel = (panelIndex) => { const mobile = isMobileSpread(); const panels = Array.from(document.querySelectorAll('.realm-tabs [role="tabpanel"]')); panels.forEach((panel, i) => { const active = i === panelIndex; panel.style.setProperty('display', active ? 'flex' : 'none', 'important'); panel.style.setProperty('flex-direction', 'column', 'important'); if (mobile) { panel.style.setProperty('height', active ? 'auto' : '0', 'important'); panel.style.setProperty('max-height', active ? 'none' : '0', 'important'); panel.style.setProperty('flex', active ? '1 1 auto' : '0 0 0', 'important'); panel.style.setProperty('min-height', active ? '0' : '0', 'important'); } else { panel.style.setProperty('height', active ? '565px' : '0', 'important'); panel.style.setProperty('max-height', active ? '565px' : '0', 'important'); panel.style.setProperty('flex', '', 'important'); } panel.style.setProperty('overflow', 'hidden', 'important'); panel.setAttribute('aria-hidden', active ? 'false' : 'true'); }); wirePageExpand(); wireMobileSpreads(); const pad = document.getElementById('aether-walk-pad'); if (pad && panelIndex !== 1) { pad.classList.remove('visible', 'explore-only'); } }; const resetActiveSpreadLeaf = () => { const panels = document.querySelectorAll('.realm-tabs [role="tabpanel"]'); const activePanel = panels[currentSpread]; if (!activePanel) return; const row = activePanel.querySelector('.tome-spread-row'); if (!row) return; row.classList.remove('tome-leaf-right'); row.classList.add('tome-leaf-left'); }; const wireMobileSpreads = () => { const mobile = isMobileSpread(); syncMobileClass(); document.querySelectorAll('.tome-spread-shell').forEach((shell) => { const row = shell.querySelector('.tome-spread-row'); if (!row) return; if (!mobile) { row.classList.remove('tome-leaf-left', 'tome-leaf-right'); shell.querySelector('.tome-leaf-bar')?.remove(); return; } if (!row.classList.contains('tome-leaf-left') && !row.classList.contains('tome-leaf-right')) { row.classList.add('tome-leaf-left'); } let bar = shell.querySelector('.tome-leaf-bar'); if (!bar) { bar = document.createElement('div'); bar.className = 'tome-leaf-bar'; bar.innerHTML = ` `; shell.appendChild(bar); const syncLeafBar = () => { const onLeft = row.classList.contains('tome-leaf-left'); bar.querySelectorAll('.tome-leaf-btn').forEach((btn) => { const active = btn.dataset.leaf === (onLeft ? 'left' : 'right'); btn.classList.toggle('is-active', active); }); bar.querySelectorAll('.tome-leaf-dots i').forEach((dot, idx) => { dot.classList.toggle('active', onLeft ? idx === 0 : idx === 1); }); }; bar.querySelectorAll('.tome-leaf-btn').forEach((btn) => { btn.addEventListener('click', () => { const leaf = btn.dataset.leaf; row.classList.remove('tome-leaf-left', 'tome-leaf-right'); row.classList.add(leaf === 'right' ? 'tome-leaf-right' : 'tome-leaf-left'); syncLeafBar(); }); }); bar._syncLeafBar = syncLeafBar; } bar._syncLeafBar?.(); }); }; if (!window._aetherMobileBound) { window._aetherMobileBound = true; MOBILE_QUERY.addEventListener('change', () => { wireMobileSpreads(); setActivePanel(currentSpread >= 0 ? sections[currentSpread].panel : 0); }); window.addEventListener('resize', () => { wireMobileSpreads(); }, { passive: true }); } const ensureAmbientMelody = () => { if (window._aetherAmbientStarted) { window._aetherAmbientCtx?.resume?.(); return; } try { const AudioCtx = window.AudioContext || window.webkitAudioContext; if (!AudioCtx) return; const ctx = new AudioCtx(); window._aetherAmbientCtx = ctx; const master = ctx.createGain(); master.gain.value = 0.0001; const warmth = ctx.createBiquadFilter(); warmth.type = 'lowpass'; warmth.frequency.value = prefersReduced ? 1600 : 2400; warmth.Q.value = 0.5; const delay = ctx.createDelay(2.8); delay.delayTime.value = 0.48; const delayFb = ctx.createGain(); delayFb.gain.value = 0.26; const delayMix = ctx.createGain(); delayMix.gain.value = 0.32; warmth.connect(delay); delay.connect(delayFb); delayFb.connect(delay); delay.connect(delayMix); delayMix.connect(master); warmth.connect(master); master.connect(ctx.destination); const now = ctx.currentTime; master.gain.linearRampToValueAtTime(prefersReduced ? 0.009 : 0.013, now + 5); const padNotes = [220, 261.63, 293.66, 329.63, 392, 440]; padNotes.forEach((freq, i) => { const osc = ctx.createOscillator(); const osc2 = ctx.createOscillator(); const g = ctx.createGain(); const g2 = ctx.createGain(); osc.type = 'sine'; osc2.type = 'triangle'; osc.frequency.value = freq; osc2.frequency.value = freq * 1.0015; const base = 0.0007 / (i * 0.55 + 1); g.gain.value = base; g2.gain.value = base * 0.45; osc.connect(g); osc2.connect(g2); g.connect(warmth); g2.connect(warmth); osc.start(); osc2.start(); const breathe = () => { const t = ctx.currentTime; g.gain.cancelScheduledValues(t); g.gain.setValueAtTime(g.gain.value, t); g.gain.linearRampToValueAtTime(base * 2.1, t + 4 + i * 0.4); g.gain.linearRampToValueAtTime(base * 0.55, t + 10 + i * 0.35); }; breathe(); setInterval(breathe, 9000 + i * 800); }); const melodyNotes = [ 392, 440, 493.88, 523.25, 587.33, 659.25, 587.33, 523.25, 440, 392, 329.63, 293.66, 329.63, 392 ]; let noteIdx = 0; const playBell = () => { if (!window._aetherAmbientStarted) return; const freq = melodyNotes[noteIdx % melodyNotes.length]; noteIdx += 1; const t = ctx.currentTime; const osc = ctx.createOscillator(); const env = ctx.createGain(); osc.type = 'sine'; osc.frequency.value = freq; env.gain.setValueAtTime(0.0001, t); env.gain.exponentialRampToValueAtTime(0.0065, t + 0.12); env.gain.exponentialRampToValueAtTime(0.0001, t + 3.2); osc.connect(env); env.connect(warmth); osc.start(t); osc.stop(t + 3.4); }; const scheduleMelody = () => { if (!window._aetherAmbientStarted) return; playBell(); setTimeout(scheduleMelody, 2600 + Math.random() * 2400); }; setTimeout(scheduleMelody, 1800); const shimmer = () => { if (!window._aetherAmbientStarted) return; const t = ctx.currentTime; const osc = ctx.createOscillator(); const env = ctx.createGain(); osc.type = 'triangle'; osc.frequency.value = 880 + Math.random() * 264; env.gain.setValueAtTime(0.0001, t); env.gain.exponentialRampToValueAtTime(0.0028, t + 0.6); env.gain.exponentialRampToValueAtTime(0.0001, t + 5); osc.connect(env); env.connect(warmth); osc.start(t); osc.stop(t + 5.2); setTimeout(shimmer, 14000 + Math.random() * 10000); }; setTimeout(shimmer, 6000); window._aetherAmbientStarted = true; document.addEventListener('visibilitychange', () => { if (document.visibilityState === 'visible') ctx.resume(); }, { passive: true }); } catch (_) {} }; const playOpeningChime = () => { try { const AudioCtx = window.AudioContext || window.webkitAudioContext; if (!AudioCtx) return; const ctx = new AudioCtx(); const now = ctx.currentTime; [523.25, 659.25, 783.99].forEach((freq, i) => { const osc = ctx.createOscillator(); const gain = ctx.createGain(); osc.type = i === 0 ? 'triangle' : 'sine'; osc.frequency.value = freq; gain.gain.setValueAtTime(0.0001, now); gain.gain.exponentialRampToValueAtTime(0.06 / (i + 1), now + 0.03 + i * 0.04); gain.gain.exponentialRampToValueAtTime(0.0001, now + 0.78 + i * 0.1); osc.connect(gain); gain.connect(ctx.destination); osc.start(now + i * 0.04); osc.stop(now + 0.95 + i * 0.08); }); } catch (_) {} }; const applyTurnFx = (direction) => { if (prefersReduced) return; const root = document.querySelector('.gradio-container'); if (!root) return; root.classList.remove('tome-turn-next', 'tome-turn-prev'); root.classList.add(direction === 'prev' ? 'tome-turn-prev' : 'tome-turn-next'); setTimeout(() => root.classList.remove('tome-turn-next', 'tome-turn-prev'), 720); }; const updatePageBadge = () => { const page = document.getElementById('tome-page-indicator'); if (!page) return; const idx = Math.max(1, currentSpread + 1); page.textContent = `Page ${toRoman(idx)}`; }; const closeToc = () => { const toc = document.getElementById('realm-toc'); if (toc) toc.classList.remove('open'); const floating = document.getElementById('tome-floating-toc'); if (floating) floating.classList.remove('open'); }; const closeBook = () => { const gate = document.getElementById('realm-opening'); const book3d = document.getElementById('realm-book-3d'); if (!gate) return; setAppInteractive(false); gate.classList.remove('is-closed', 'is-opened', 'is-prologue', 'is-unfolded', 'is-unfolding'); gate.classList.add('is-closing', 'is-reclosed'); const openBook = document.getElementById('realm-book-open'); const prologue = document.getElementById('realm-prologue'); if (openBook) openBook.setAttribute('aria-hidden', 'true'); if (prologue) { prologue.classList.remove('visible'); prologue.setAttribute('aria-hidden', 'true'); } if (book3d) { book3d.classList.remove('is-opening'); setTimeout(() => book3d.classList.add('show-back'), prefersReduced ? 0 : 80); } setTimeout(() => { gate.classList.remove('is-closing'); gate.classList.add('is-back-visible'); }, prefersReduced ? 60 : 1150); applyTurnFx('prev'); }; const reopenBook = () => { const gate = document.getElementById('realm-opening'); const book3d = document.getElementById('realm-book-3d'); if (!gate) return; if (book3d) book3d.classList.remove('show-back', 'is-opening'); gate.classList.remove('is-reclosed', 'is-back-visible', 'is-closing'); gate.classList.add('is-opened', 'is-closed'); setAppInteractive(true); playOpeningChime(); ensureAmbientMelody(); }; const goToSpread = (idx, direction = 'next') => { if (idx >= sections.length && direction === 'next') { closeBook(); return; } const clamped = Math.max(0, Math.min(sections.length - 1, idx)); const spread = sections[clamped]; if (!spread) return; applyTurnFx(direction); setActivePanel(spread.panel); resetActiveSpreadLeaf(); wireMobileSpreads(); currentSpread = clamped; window._aetherCurrentSpread = spread.key; updatePageBadge(); closeToc(); updateWalkPadVisibility(); const gate = document.getElementById('realm-opening'); if (gate && gate.classList.contains('is-opened')) { gate.classList.add('is-closed'); } }; const wireTomeNav = () => { if (window._tomeNavBound) return; window._tomeNavBound = true; const root = document.querySelector('.gradio-container'); if (root && !document.getElementById('tome-nav')) { const nav = document.createElement('div'); nav.id = 'tome-nav'; nav.innerHTML = `
Page I
${sections.map((s, i) => ``).join('')}
`; document.body.appendChild(nav); } const prev = document.getElementById('tome-prev'); const next = document.getElementById('tome-next'); const tocOpen = document.getElementById('tome-toc-open'); const jump = document.getElementById('tome-jump'); if (prev && !prev.dataset.bound) { prev.dataset.bound = '1'; prev.addEventListener('click', () => goToSpread(currentSpread - 1, 'prev')); } if (next && !next.dataset.bound) { next.dataset.bound = '1'; next.addEventListener('click', () => { if (currentSpread >= sections.length - 1) { closeBook(); return; } goToSpread(currentSpread + 1, 'next'); }); } if (tocOpen && !tocOpen.dataset.bound) { tocOpen.dataset.bound = '1'; tocOpen.addEventListener('click', () => { const floating = document.getElementById('tome-floating-toc'); if (floating) floating.classList.toggle('open'); }); } if (jump && !jump.dataset.bound) { jump.dataset.bound = '1'; jump.addEventListener('change', () => goToSpread(Number(jump.value || 0), 'next')); } document.querySelectorAll('.realm-toc-link').forEach((btn, idx) => { if (btn.dataset.bound) return; btn.dataset.bound = '1'; btn.addEventListener('click', () => goToSpread(idx, idx < currentSpread ? 'prev' : 'next')); }); document.querySelectorAll('.tome-floating-link').forEach((btn) => { if (btn.dataset.bound) return; btn.dataset.bound = '1'; btn.addEventListener('click', () => { const idx = Number(btn.dataset.index || 0); goToSpread(idx, idx < currentSpread ? 'prev' : 'next'); }); }); window.addEventListener('keydown', (e) => { const tag = document.activeElement?.tagName; if (tag === 'INPUT' || tag === 'TEXTAREA' || tag === 'SELECT') return; if (e.key === 'ArrowRight') { if (currentSpread >= sections.length - 1) closeBook(); else goToSpread(currentSpread + 1, 'next'); } if (e.key === 'ArrowLeft') goToSpread(currentSpread - 1, 'prev'); if (e.key === 'Escape') closeToc(); }, { passive: true }); updatePageBadge(); window._aetherCurrentSpread = sections[currentSpread].key; setActivePanel(sections[currentSpread].panel); }; const wirePageExpand = () => { document.querySelectorAll('.tome-expand-btn, #tome-expand-backdrop').forEach((el) => { el.remove(); }); }; const setAppInteractive = (on) => { const app = document.querySelector('.gradio-container'); if (app) app.style.pointerEvents = on ? '' : 'none'; }; const wireOpening = () => { const gate = document.getElementById('realm-opening'); if (!gate || gate.dataset.bound) return; gate.dataset.bound = '1'; if (gate.classList.contains('is-open') && !gate.classList.contains('is-opened')) { setAppInteractive(false); } const book3d = document.getElementById('realm-book-3d'); const closedBook = document.getElementById('realm-book-closed'); const openBook = document.getElementById('realm-book-open'); const prologue = document.getElementById('realm-prologue'); let prologueReady = false; let prologueTypingGen = 0; const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms)); const typeText = (el, text, speed = 24, gen = 0) => new Promise((resolve) => { if (!el || !text) { resolve(); return; } let i = 0; el.textContent = ''; el.classList.add('is-typing'); const tick = () => { if (gen !== prologueTypingGen) { el.classList.remove('is-typing'); resolve(); return; } if (i < text.length) { el.textContent += text[i++]; setTimeout(tick, speed + Math.random() * 18); } else { el.classList.remove('is-typing'); resolve(); } }; tick(); }); const setPrologueReady = () => { prologueReady = true; const btn = document.getElementById('realm-prologue-enter'); const skipBtn = document.getElementById('realm-prologue-skip'); const cursor = document.getElementById('realm-prologue-cursor'); if (cursor) cursor.classList.remove('active'); if (skipBtn) skipBtn.classList.add('is-hidden'); if (btn) { btn.disabled = false; btn.classList.add('ready'); } }; const skipPrologue = () => { if (prologueReady) return; prologueTypingGen += 1; const kicker = document.getElementById('realm-prologue-kicker'); const myth = document.getElementById('realm-prologue-myth'); const whisper = document.getElementById('realm-prologue-whisper'); if (kicker) { kicker.textContent = kicker.dataset.text || ''; kicker.classList.remove('is-typing'); } if (myth) { myth.textContent = myth.dataset.text || ''; myth.classList.remove('is-typing'); } if (whisper) { whisper.textContent = whisper.dataset.text || ''; whisper.classList.remove('is-typing'); } setPrologueReady(); }; const runPrologueTyping = async () => { const kicker = document.getElementById('realm-prologue-kicker'); const myth = document.getElementById('realm-prologue-myth'); const whisper = document.getElementById('realm-prologue-whisper'); const skipBtn = document.getElementById('realm-prologue-skip'); const cursor = document.getElementById('realm-prologue-cursor'); const gen = ++prologueTypingGen; if (skipBtn) skipBtn.classList.remove('is-hidden'); if (cursor) cursor.classList.add('active'); if (prefersReduced) { skipPrologue(); return; } await typeText(kicker, kicker?.dataset.text || '', 34, gen); if (gen !== prologueTypingGen) return; await sleep(380); await typeText(myth, myth?.dataset.text || '', 20, gen); if (gen !== prologueTypingGen) return; await sleep(420); await typeText(whisper, whisper?.dataset.text || '', 22, gen); if (gen !== prologueTypingGen) return; setPrologueReady(); }; const hideOpeningShell = () => { const host = document.getElementById('realm-opening-host'); if (host) { host.style.setProperty('display', 'none', 'important'); host.style.setProperty('visibility', 'hidden', 'important'); host.style.setProperty('pointer-events', 'none', 'important'); } const hoisted = document.getElementById('realm-opening'); if (hoisted) { hoisted.classList.add('is-closed'); hoisted.style.setProperty('pointer-events', 'none', 'important'); } document.body.classList.add('aether-realm-entered'); }; const revealRealm = () => { setAppInteractive(true); setActivePanel(sections[currentSpread].panel); updatePageBadge(); const nav = document.getElementById('tome-nav'); if (nav) nav.style.removeProperty('display'); }; const finishOpening = () => { if (!prologueReady) return; playOpeningChime(); ensureAmbientMelody(); gate.classList.remove('is-reclosed'); gate.classList.add('is-opened'); hideOpeningShell(); revealRealm(); closeToc(); setTimeout(() => { gate.classList.add('is-closed'); hideOpeningShell(); revealRealm(); }, 700); }; const beginUnfold = () => { playOpeningChime(); ensureAmbientMelody(); setAppInteractive(false); gate.classList.add('is-unfolding'); if (book3d) book3d.classList.add('is-opening'); if (openBook) openBook.setAttribute('aria-hidden', 'false'); setTimeout(() => { gate.classList.add('is-unfolded', 'is-prologue'); if (prologue) { prologue.setAttribute('aria-hidden', 'false'); prologue.classList.add('visible'); } runPrologueTyping(); }, prefersReduced ? 120 : 1200); }; gate.addEventListener('click', (e) => { const target = e.target; if (!(target instanceof Element)) return; if (gate.classList.contains('is-reclosed') && (target.id === 'realm-opening-enter' || target.id === 'realm-reopen-enter')) { e.preventDefault(); e.stopPropagation(); reopenBook(); return; } if (target.id === 'realm-opening-enter') { e.preventDefault(); e.stopPropagation(); beginUnfold(); } if (target.id === 'realm-prologue-skip') { e.preventDefault(); e.stopPropagation(); skipPrologue(); return; } if (target.id === 'realm-prologue-enter' && prologueReady) { e.preventDefault(); e.stopPropagation(); finishOpening(); } }); window.addEventListener('keydown', (e) => { if (!prologueReady || !gate.classList.contains('is-prologue')) return; const tag = document.activeElement?.tagName; if (tag === 'INPUT' || tag === 'TEXTAREA' || tag === 'SELECT') return; if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); finishOpening(); } }, { passive: false }); }; const dedupeOpening = () => { const hoisted = document.body.querySelector('#realm-opening.realm-opening-hoisted') || document.body.querySelector('#realm-opening'); if (hoisted) { document.querySelectorAll('#realm-opening').forEach((el) => { if (el !== hoisted) el.remove(); }); } const host = document.getElementById('realm-opening-host'); if (host) { host.querySelectorAll('#realm-opening').forEach((el) => el.remove()); if (!host.querySelector('#realm-opening')) { host.style.setProperty('display', 'none', 'important'); host.style.setProperty('visibility', 'hidden', 'important'); host.style.setProperty('pointer-events', 'none', 'important'); } } }; const hoistOpening = () => { if (window._aetherOpeningHoisted) { dedupeOpening(); return; } const gate = document.getElementById('realm-opening'); const critical = document.getElementById('realm-opening-critical'); const host = document.getElementById('realm-opening-host'); if (gate && gate.parentElement !== document.body) { window._aetherOpeningHoisted = true; gate.classList.add('realm-opening-hoisted'); document.body.appendChild(gate); if (critical && critical.parentElement !== document.head) { document.head.appendChild(critical); } } else if (gate && gate.parentElement === document.body) { window._aetherOpeningHoisted = true; gate.classList.add('realm-opening-hoisted'); } dedupeOpening(); if (host && !host.querySelector('#realm-opening')) { host.style.setProperty('display', 'none', 'important'); host.style.setProperty('visibility', 'hidden', 'important'); host.style.setProperty('pointer-events', 'none', 'important'); } }; hoistOpening(); wireOpening(); wireTomeNav(); wireMobileSpreads(); wirePageExpand(); const gateOnLoad = document.getElementById('realm-opening'); if (gateOnLoad && gateOnLoad.classList.contains('is-open') && !gateOnLoad.classList.contains('is-opened')) { setAppInteractive(false); } const triggerGradioInput = (elemId, value) => { const clean = String(elemId || '').replace(/^#/, ''); let root = document.getElementById(clean) || document.querySelector('#' + clean) || document.querySelector('[id="' + clean + '"]'); if (!root) { const cfg = window.gradio_config?.components || []; const comp = cfg.find((c) => c?.props?.elem_id === clean); if (comp) root = document.getElementById('component-' + comp.id); } if (!root) { root = document.querySelector('.aether-hidden-pick#' + clean) || document.querySelector('.aether-hidden-pick[id="' + clean + '"]'); } if (!root) return false; const input = (root.tagName === 'TEXTAREA' || root.tagName === 'INPUT') ? root : (root.querySelector('textarea:not([disabled])') || root.querySelector('input[type="text"]') || root.querySelector('input:not([type="hidden"])')); if (!input) return false; const proto = input.tagName === 'TEXTAREA' ? HTMLTextAreaElement.prototype : HTMLInputElement.prototype; const setter = Object.getOwnPropertyDescriptor(proto, 'value')?.set; const next = String(value); if (setter) setter.call(input, next); else input.value = next; input.dispatchEvent(new Event('input', { bubbles: true, composed: true })); input.dispatchEvent(new Event('change', { bubbles: true, composed: true })); return true; }; const ensureLocationModal = () => { let modal = document.getElementById('aether-location-modal'); if (modal) return modal; modal = document.createElement('div'); modal.id = 'aether-location-modal'; modal.className = 'location-modal-overlay'; modal.innerHTML = `

`; document.body.appendChild(modal); const closeBtn = modal.querySelector('.location-modal-close'); if (closeBtn) { closeBtn.addEventListener('click', () => modal.classList.remove('is-open')); closeBtn.addEventListener('keydown', (e) => { if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); modal.classList.remove('is-open'); } }); } modal.addEventListener('click', (e) => { if (e.target === modal) modal.classList.remove('is-open'); }); return modal; }; const openLocationFromNode = (node) => { if (!node) return; const id = node.getAttribute('data-location-id'); if (!id) return; const modal = ensureLocationModal(); const name = node.getAttribute('data-name') || 'Unknown place'; const desc = node.getAttribute('data-desc') || ''; const special = node.getAttribute('data-special') || ''; const souls = node.getAttribute('data-souls') || '0'; const img = node.getAttribute('data-img') || ''; const nameEl = modal.querySelector('.location-panel-name'); const descEl = modal.querySelector('.location-panel-desc'); const specialEl = modal.querySelector('.location-panel-special'); const soulsEl = modal.querySelector('.location-soul-count'); const imgEl = modal.querySelector('.location-panel-image'); const imgWrap = modal.querySelector('.location-panel-image-wrap'); if (nameEl) nameEl.textContent = name; if (descEl) descEl.textContent = desc; if (specialEl) { specialEl.textContent = special ? ('✦ ' + special) : ''; specialEl.style.display = special ? '' : 'none'; } if (soulsEl) soulsEl.textContent = souls + ' souls currently here'; if (imgEl) { imgEl.src = img || ''; imgEl.alt = name; } if (imgWrap) imgWrap.style.display = img ? '' : 'none'; modal.classList.add('is-open'); document.querySelectorAll('.location-node').forEach((n) => { n.classList.toggle('location-selected', n.getAttribute('data-location-id') === id); }); triggerGradioInput('map_location_pick', id); }; const bind = () => { document.querySelectorAll('#world-map .location-node, [id="world-map"] .location-node').forEach((node) => { node.style.cursor = 'pointer'; }); }; if (!window._aetherMapDelegationBound) { window._aetherMapDelegationBound = true; const mapPlaceTarget = (target) => { if (!(target instanceof Element)) return null; const hit = target.closest('.location-hit, .location-circle, .location-label, .location-count'); if (!hit) return null; const node = hit.closest('.location-node[data-location-id]'); if (!node) return null; const inMap = node.closest('#world-map') || node.closest('[id="world-map"]'); return inMap ? node : null; }; document.addEventListener('click', (e) => { const node = mapPlaceTarget(e.target); if (!node) return; e.preventDefault(); e.stopPropagation(); openLocationFromNode(node); }); } const fireHiddenInput = (id, value) => { const elemId = (id || '').replace(/^#/, ''); triggerGradioInput(elemId, value); }; const wireClickable = (el, handler) => { if (!el || el.dataset.boundClick) return; el.dataset.boundClick = '1'; el.addEventListener('click', handler); el.addEventListener('keydown', (e) => { if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); handler(e); } }); }; const closeLocationModal = () => { document.querySelectorAll('.location-modal-overlay').forEach((m) => m.classList.remove('is-open')); }; window._aetherCloseLocationModal = closeLocationModal; const bindSpreadClicks = () => { document.querySelectorAll('.place-ribbon-item').forEach((btn) => { wireClickable(btn, () => { const id = btn.getAttribute('data-location-id'); if (!id) return; document.querySelectorAll('.place-ribbon-item').forEach((el) => { el.classList.toggle('is-selected', el.getAttribute('data-location-id') === id); }); fireHiddenInput('explore_place_pick', id); if (window.matchMedia('(max-width: 920px)').matches) { requestAnimationFrame(() => { const stage = document.querySelector('.tome-explore-stage .diorama-frame') || document.querySelector('.tome-explore-stage'); stage?.scrollIntoView({ behavior: 'smooth', block: 'nearest' }); }); } }); }); document.querySelectorAll('.day-tab').forEach((btn) => { wireClickable(btn, () => { const day = btn.getAttribute('data-day'); if (day) fireHiddenInput('#book_day_pick', day); }); }); document.querySelectorAll('.entity-grid-card').forEach((card) => { const id = card.getAttribute('data-entity-id'); if (!id) return; card.style.cursor = 'pointer'; if (card.dataset.entityBound === id) return; card.dataset.entityBound = id; wireClickable(card, () => { document.querySelectorAll('.entity-grid-card').forEach((el) => { el.classList.toggle('is-selected', el.getAttribute('data-entity-id') === id); }); fireHiddenInput('entity_pick', id); }); }); document.querySelectorAll('.location-modal-close').forEach((btn) => { wireClickable(btn, closeLocationModal); }); document.querySelectorAll('.location-modal-overlay').forEach((overlay) => { if (overlay.dataset.overlayBound) return; overlay.dataset.overlayBound = '1'; overlay.addEventListener('click', (e) => { if (e.target === overlay) closeLocationModal(); }); }); }; bind(); bindSpreadClicks(); new MutationObserver(() => { bind(); bindSpreadClicks(); }).observe(document.body, { childList: true, subtree: true }); window._aetherHoverDiorama = false; window._aetherDioramaActive = false; window._aetherActiveFrame = null; const GAME_KEYS = new Set([ 'KeyW','KeyA','KeyS','KeyD', 'ArrowUp','ArrowDown','ArrowLeft','ArrowRight', 'KeyE','KeyQ','ShiftLeft','ShiftRight','Escape' ]); const getVisibleDioramaFrame = () => { const frames = Array.from(document.querySelectorAll('iframe.diorama-frame')); let best = null; let bestArea = 0; for (const frame of frames) { const rect = frame.getBoundingClientRect(); if (rect.width < 40 || rect.height < 40) continue; const area = rect.width * rect.height; if (area > bestArea) { bestArea = area; best = frame; } } return best || frames[frames.length - 1] || null; }; const sendKeyToFrame = (frame, code, action) => { if (!frame) return; try { frame.contentWindow?.postMessage({ type: 'aether_key', code, action }, '*'); } catch (_) {} }; const sendWalkToFrame = (frame, fwd, strafe) => { if (!frame) return; try { frame.contentWindow?.postMessage({ type: 'aether_walk', fwd, strafe }, '*'); } catch (_) {} }; window._walkHeld = window._walkHeld || new Set(); const walkVectorFromHeld = () => { let fwd = 0, strafe = 0; if (window._walkHeld.has('KeyW')) fwd += 1; if (window._walkHeld.has('KeyS')) fwd -= 1; if (window._walkHeld.has('KeyA')) strafe -= 1; if (window._walkHeld.has('KeyD')) strafe += 1; if (fwd !== 0 && strafe !== 0) { const n = Math.hypot(fwd, strafe); fwd /= n; strafe /= n; } return { fwd, strafe }; }; const syncWalkToVisibleFrame = () => { const frame = getVisibleDioramaFrame() || window._aetherActiveFrame; if (!frame) return; const { fwd, strafe } = walkVectorFromHeld(); sendWalkToFrame(frame, fwd, strafe); }; const ensureGlobalWalkPad = () => { let el = document.getElementById('aether-walk-pad'); if (!el) { el = document.createElement('div'); el.id = 'aether-walk-pad'; el.setAttribute('aria-label', 'Walk'); el.innerHTML = `
`; } const stage = getVisibleDioramaFrame()?.closest('.diorama-stage'); if (stage && el.parentElement !== stage) { stage.appendChild(el); } }; const updateWalkPadVisibility = () => { ensureGlobalWalkPad(); const pad = document.getElementById('aether-walk-pad'); if (!pad) return; const frame = getVisibleDioramaFrame(); const stage = frame?.closest('.diorama-stage'); if (stage && pad.parentElement !== stage) stage.appendChild(pad); const show = !!frame && !!stage && !pad.classList.contains('controls-hidden') && window._aetherCurrentSpread === 'explore'; pad.classList.toggle('visible', show); pad.classList.toggle('explore-only', show); }; const wireWalkPad = () => { ensureGlobalWalkPad(); document.querySelectorAll('#aether-walk-pad .awp-btn').forEach((btn) => { if (btn.dataset.walkWired) return; btn.dataset.walkWired = '1'; const code = btn.dataset.key; if (!code) return; const down = (e) => { e.preventDefault(); e.stopPropagation(); btn.classList.add('pressed'); window._walkHeld.add(code); try { btn.setPointerCapture(e.pointerId); } catch (_) {} syncWalkToVisibleFrame(); }; const up = (e) => { e.preventDefault(); btn.classList.remove('pressed'); window._walkHeld.delete(code); syncWalkToVisibleFrame(); }; btn.addEventListener('pointerdown', down); btn.addEventListener('pointerup', up); btn.addEventListener('pointercancel', up); btn.addEventListener('lostpointercapture', up); }); updateWalkPadVisibility(); }; const wireDioramaFrames = () => { document.querySelectorAll('.diorama-wrap').forEach((wrap) => { if (wrap.dataset.dioramaWrapBound) return; wrap.dataset.dioramaWrapBound = '1'; const frame = wrap.querySelector('iframe.diorama-frame'); const activate = () => { window._aetherHoverDiorama = true; window._aetherActiveFrame = frame || window._aetherActiveFrame; if (frame) { try { frame.focus({ preventScroll: true }); } catch (_) {} try { frame.contentWindow?.postMessage({ type: 'aether_activate' }, '*'); } catch (_) {} } }; wrap.addEventListener('mouseenter', activate); wrap.addEventListener('mousemove', () => { window._aetherHoverDiorama = true; window._aetherActiveFrame = frame || window._aetherActiveFrame; }); wrap.addEventListener('mouseleave', () => { if (!window._aetherDioramaActive) window._aetherHoverDiorama = false; }); wrap.addEventListener('mousedown', activate); wrap.addEventListener('click', activate); }); document.querySelectorAll('iframe.diorama-frame').forEach((frame) => { if (frame.dataset.dioramaBound) return; frame.dataset.dioramaBound = '1'; frame.setAttribute('tabindex', '0'); window._aetherActiveFrame = frame; frame.addEventListener('load', () => { window._aetherActiveFrame = frame; setTimeout(() => { try { frame.contentWindow?.postMessage('diorama-resize', '*'); } catch (_) {} }, 120); setTimeout(() => { try { frame.contentWindow?.postMessage('diorama-resize', '*'); } catch (_) {} }, 600); setTimeout(() => { try { frame.contentWindow?.postMessage({ type: 'aether_activate' }, '*'); } catch (_) {} }, 200); }); }); }; wireDioramaFrames(); wireWalkPad(); new MutationObserver(() => { wireDioramaFrames(); wireWalkPad(); updateWalkPadVisibility(); wirePageExpand(); }).observe(document.body, { childList: true, subtree: true }); setInterval(updateWalkPadVisibility, 800); if (!window._walkHeldSyncBound) { window._walkHeldSyncBound = true; document.addEventListener('pointerup', () => { document.querySelectorAll('#aether-walk-pad .awp-btn.pressed').forEach((btn) => { btn.classList.remove('pressed'); window._walkHeld.delete(btn.dataset.key); }); syncWalkToVisibleFrame(); }, true); setInterval(() => { if (window._walkHeld.size) syncWalkToVisibleFrame(); }, 40); } if (!window._aetherKeyForwardBound) { window._aetherKeyForwardBound = true; const typingInForm = () => { const el = document.activeElement; if (!el) return false; const tag = el.tagName; if (tag === 'INPUT' || tag === 'TEXTAREA' || tag === 'SELECT') return true; if (el.isContentEditable) return true; if (el.closest?.('textarea, input, [contenteditable="true"]')) return true; const frame = getActiveFrame(); try { const iel = frame?.contentDocument?.activeElement; if (iel && (iel.tagName === 'INPUT' || iel.tagName === 'TEXTAREA' || iel.isContentEditable)) return true; } catch (_) {} return false; }; const getActiveFrame = () => getVisibleDioramaFrame() || window._aetherActiveFrame || null; const MOVE_KEYS = new Set([ 'KeyW','KeyA','KeyS','KeyD', 'ArrowUp','ArrowDown','ArrowLeft','ArrowRight' ]); const forwardKey = (e, action) => { if (!GAME_KEYS.has(e.code)) return; if (typingInForm()) return; const frame = getActiveFrame(); if (!frame) return; const rect = frame.getBoundingClientRect(); if (rect.width < 40 || rect.height < 40) return; e.preventDefault(); e.stopImmediatePropagation(); if (MOVE_KEYS.has(e.code)) { if (action === 'down') window._walkHeld.add(e.code); else window._walkHeld.delete(e.code); syncWalkToVisibleFrame(); return; } sendKeyToFrame(frame, e.code, action); }; window.addEventListener('keydown', (e) => forwardKey(e, 'down'), true); window.addEventListener('keyup', (e) => forwardKey(e, 'up'), true); } if (!window._aetherFocusRecheckBound) { window._aetherFocusRecheckBound = true; setInterval(() => { if (!window._aetherDioramaActive) return; const el = document.activeElement; const typing = el && ( el.tagName === 'INPUT' || el.tagName === 'TEXTAREA' || el.tagName === 'SELECT' || el.isContentEditable ); if (typing) return; const frame = window._aetherActiveFrame || document.querySelector('iframe.diorama-frame'); if (!frame) return; try { frame.contentWindow?.postMessage({ type: 'aether_activate' }, '*'); } catch (_) {} }, 12000); } // Portal navigation from diorama iframe if (!window._aetherPortalListenerBound) { window._aetherPortalListenerBound = true; window.addEventListener('message', (e) => { if (e.data && e.data.type === 'aether_register_frame') { const frames = Array.from(document.querySelectorAll('iframe.diorama-frame')); const active = frames.find((frame) => frame.contentWindow === e.source); if (active) { window._aetherActiveFrame = active; window._aetherHoverDiorama = true; window._aetherDioramaActive = true; } return; } if (e.data && e.data.type === 'aether_diorama_active') { const frames = Array.from(document.querySelectorAll('iframe.diorama-frame')); const active = frames.find((frame) => frame.contentWindow === e.source); if (active) { window._aetherActiveFrame = active; window._aetherHoverDiorama = true; window._aetherDioramaActive = true; } return; } if (e.data && e.data.type === 'aether_profile') { const pad = document.getElementById('aether-walk-pad'); if (pad) pad.classList.toggle('controls-hidden', !!e.data.open); if (e.data.open) { window._walkHeld.clear(); syncWalkToVisibleFrame(); } updateWalkPadVisibility(); return; } if (!e.data || e.data.type !== 'aether_portal') return; const id = String(e.data.locationId || ''); if (!id) return; // Try explore tab portal_pick first const pp = document.querySelector('#portal_location_pick'); const ppInput = pp && (pp.querySelector('textarea') || pp.querySelector('input')); if (ppInput) { ppInput.value = id; ppInput.dispatchEvent(new Event('input', { bubbles: true })); } // Also update map_pick to sync realm tab const mp = document.querySelector('#map_location_pick'); const mpInput = mp && (mp.querySelector('textarea') || mp.querySelector('input')); if (mpInput) { mpInput.value = id; mpInput.dispatchEvent(new Event('input', { bubbles: true })); } }); } } """ demo.load(None, None, None, js=MAP_CLICK_JS) return demo if __name__ == "__main__": demo = build_app() launch_kwargs: dict = { "server_name": "0.0.0.0", "server_port": int(os.environ.get("PORT", 7860)), "share": False, "allowed_paths": assets.allowed_paths() + [ str(Path(tempfile.gettempdir()) / "aether_soul_cards"), str(Path(__file__).parent / "assets" / "generated"), ], } if GRADIO_MAJOR >= 6: launch_kwargs["css"] = CUSTOM_CSS launch_kwargs["theme"] = REALM_THEME demo.launch(**launch_kwargs)