"""Aether Garden — main Gradio application.""" from __future__ import annotations import os import tempfile import uuid 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_of_ages, render_current_event from ui.entity_card import render_entity_card, render_entity_grid 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_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." ) 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 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 location on the map to explore.

' location = get_location_by_id(location_id) if not location: return "" entities = get_entities_by_location(location_id) chips = "".join( f'{e["display_name"]}' for e in entities[:8] ) more = f'+{len(entities) - 8} more' if len(entities) > 8 else "" img = assets.location_image_url(location.get("slug")) img_style = f"background-image: url('{img}');" if img else "" special = location.get("special_property", "") return f"""

{location['name']}

{location['short_description']}

✦ {special}

{len(entities)} souls currently here

{chips}{more}
""" _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, "", ) 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_diorama(entity["location_id"]), ) 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) def filter_book(entry_type, search): return render_book_of_ages(entry_type=entry_type, search=search) 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." 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"), player_message=player_message.strip()[:300], ) try: response = generate( user_prompt=user_prompt, system_prompt=SOUL_CONVERSATION_SYSTEM, max_new_tokens=180, temperature=0.88, ) return response.strip() if response else "..." 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 enter_place(evt: gr.SelectData): ids = place_ids() if evt.index is None or evt.index >= len(ids): return None, render_diorama(None), render_room(None), gr.update(value=[]), [], _SOUL_HINT loc_id = ids[evt.index] items, soul_ids = soul_gallery(loc_id) return loc_id, render_diorama(loc_id), render_room(loc_id), gr.update(value=items), soul_ids, _SOUL_HINT def wander_to_place(): loc_id = random_place_id() if not loc_id: return None, render_diorama(None), render_room(None), gr.update(value=[]), [], _SOUL_HINT items, soul_ids = soul_gallery(loc_id) return loc_id, render_diorama(loc_id), render_room(loc_id), gr.update(value=items), soul_ids, _SOUL_HINT def open_diorama_from_map(location_id: int | None): if not location_id: return render_diorama(None), render_room(None), gr.update(value=[]), [], _SOUL_HINT items, soul_ids = soul_gallery(location_id) return render_diorama(location_id), render_room(location_id), gr.update(value=items), soul_ids, _SOUL_HINT 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())) 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_classes=["realm-tabs"]) as tabs: with gr.Tab("The Realm", id="realm"): world_vignette = gr.HTML(value=render_world_vignette()) realm_pulse = gr.HTML(value=render_realm_pulse()) 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. Open the Explore tab to step inside a place ' 'and meet who lives there.

' '
' ) with gr.Row(): with gr.Column(scale=3): world_map = gr.HTML( value=render_world_map(), elem_id="world-map", ) location_panel = gr.HTML( value=render_location_panel(None), ) realm_diorama = gr.HTML(value="") map_pick = gr.Textbox(visible=False, elem_id="map_location_pick") with gr.Column(scale=2, elem_classes=["sidebar-panel"]): current_event = gr.HTML(value=render_current_event()) activity_feed = gr.HTML(value=render_activity_feed()) 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() gr.HTML('

Summon Something Into the Realm

') with gr.Row(): summon_input = gr.Textbox( placeholder="Describe what you are bringing into the world...", label=None, show_label=False, lines=2, scale=4, ) summon_btn = gr.Button("Summon →", variant="primary", scale=1) gr.HTML( '

Hint: "A wizard" is forgotten. ' '"A wizard who collects the sound of doors closing" is remembered.

' ) entity_result = gr.HTML() with gr.Group(visible=True): soul_card_img = gr.Image( label="Your Soul Card — download & share it", visible=False, show_label=True, interactive=False, type="filepath", elem_classes=["soul-card-image"], height=420, ) share_caption_box = gr.Textbox( label="Share caption (copy this with your card)", visible=False, lines=3, interactive=True, ) location_select = gr.Dropdown( choices=[(l["name"], l["id"]) for l in get_all_locations()], label="Explore Location", visible=True, ) featured_chars = gr.HTML(value=render_featured_characters()) with gr.Tab("Explore", 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=[]) gr.HTML( '
' '

Walk the Eight Sacred Places

' '

Pick a place below — then WASD to walk, ' 'drag to look, E to talk to any soul you find. ' 'Other live visitors appear as golden wisps when they step into the same place. ' 'Or press ✦ to wander somewhere at random.

' '
' ) wander_btn = gr.Button("✦ Wander somewhere at random", variant="secondary") 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"], ) diorama_view = gr.HTML(value=render_diorama(_default_loc_id)) portal_pick = gr.Textbox(visible=False, elem_id="portal_location_pick") with gr.Accordion("Classic view (if 3D does not load)", open=False): 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"): with gr.Row(): book_filter = gr.Dropdown( choices=[ ("All", "all"), ("Arrivals", "arrival"), ("Interactions", "interaction"), ("Events", "world_event"), ("Milestones", "milestone"), ("Dreams", "dream_fragment"), ], value="all", label="Filter", scale=1, ) book_search = gr.Textbox( placeholder="Search by name...", label="Search", scale=2, ) book_display = gr.HTML(value=render_book_of_ages()) with gr.Tab("Bonds", id="bonds"): gr.HTML( '
' '

The Web of Bonds

' '

Every alliance, rivalry, and debt forged by the simulation — ' 'not scripted, only remembered.

' ) rel_graph = gr.HTML(value=render_relationship_graph()) with gr.Tab("All Entities", id="entities"): with gr.Row(): loc_filter = gr.Dropdown( choices=["all"] + [l["name"] for l in get_all_locations()], value="all", label="Location", ) type_filter = gr.Dropdown( choices=["all", "character", "creature", "object", "place"], value="all", label="Type", ) status_filter = gr.Dropdown( choices=["all", "active", "dormant", "legendary"], value="all", label="Status", ) entity_search = gr.Textbox(placeholder="Search...", label="Search") entity_grid = gr.HTML(value=render_entity_grid(get_all_entities())) entity_detail = gr.HTML() entity_names = gr.Dropdown( choices=[e["display_name"] for e in get_all_entities()], label="View Entity Profile", ) _summon_outputs = [ world_map, header, activity_feed, entity_result, location_panel, soul_card_img, share_caption_box, realm_diorama, ] 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 diorama = document.querySelector('.diorama-wrap'); const target = card || diorama || document.querySelector('.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 diorama = document.querySelector('.diorama-wrap'); const target = card || diorama || document.querySelector('.entity-card'); if (target) target.scrollIntoView({ behavior: 'smooth', block: 'center' }); }, 350); } """, ) def _realm_location_change(loc_id): d = render_diorama(loc_id) if loc_id else "" return render_world_map(loc_id), render_location_panel(loc_id), d location_select.change( fn=_realm_location_change, inputs=[location_select], outputs=[world_map, location_panel, realm_diorama], ) def _map_open_diorama(pick): try: loc_id = int(pick) if pick else None except (TypeError, ValueError): loc_id = None if not loc_id: return "", render_diorama(None), render_room(None), gr.update(value=[]), [], _SOUL_HINT items, soul_ids = soul_gallery(loc_id) return ( render_diorama(loc_id), render_diorama(loc_id), render_room(loc_id), gr.update(value=items), soul_ids, _SOUL_HINT, ) map_pick.change( _map_open_diorama, inputs=[map_pick], outputs=[realm_diorama, diorama_view, room_view, souls_gallery, soul_ids_state, soul_card], ).then( lambda pick: int(pick) if pick and str(pick).isdigit() else None, inputs=[map_pick], outputs=[explore_loc], ) portal_pick.change( open_diorama_from_map, inputs=[portal_pick], outputs=[diorama_view, room_view, souls_gallery, soul_ids_state, soul_card], ).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, diorama_view, room_view, souls_gallery, soul_ids_state, soul_card], ) wander_btn.click( wander_to_place, inputs=None, outputs=[explore_loc, diorama_view, room_view, souls_gallery, soul_ids_state, soul_card], ) 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], ) # ── 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], ) book_filter.change(filter_book, [book_filter, book_search], [book_display]) book_search.change(filter_book, [book_filter, book_search], [book_display]) 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_names.change(select_entity_from_dropdown, [entity_names], [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]) MAP_CLICK_JS = """ () => { const bind = () => { document.querySelectorAll('#world-map [data-location-id]').forEach((node) => { if (node.dataset.mapBound) return; node.dataset.mapBound = '1'; node.style.cursor = 'pointer'; node.addEventListener('click', () => { const id = node.getAttribute('data-location-id'); const root = document.querySelector('#map_location_pick'); const input = root && (root.querySelector('textarea') || root.querySelector('input')); if (input && id) { input.value = id; input.dispatchEvent(new Event('input', { bubbles: true })); } }); }); }; bind(); new MutationObserver(bind).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 analogFromKeys = (keySet) => { let fwd = 0, strafe = 0; if (keySet.has('KeyW') || keySet.has('ArrowUp')) fwd += 1; if (keySet.has('KeyS') || keySet.has('ArrowDown')) fwd -= 1; if (keySet.has('KeyA') || keySet.has('ArrowLeft')) strafe -= 1; if (keySet.has('KeyD') || keySet.has('ArrowRight')) strafe += 1; return { fwd, strafe }; }; const sendAnalogToFrame = (frame, fwd, strafe) => { if (!frame) return; try { frame.contentWindow?.postMessage({ type: 'aether_analog', fwd, strafe, fromParent: true }, '*'); } catch (_) {} }; const sendKeysToFrame = (frame, codes) => { if (!frame) return; const set = new Set(codes); try { frame.contentWindow?.postMessage({ type: 'aether_keys', keys: codes, fromParent: true }, '*'); } catch (_) {} const { fwd, strafe } = analogFromKeys(set); sendAnalogToFrame(frame, fwd, strafe); }; const sendKeyToFrame = (frame, code, action) => { if (!frame) return; try { frame.contentWindow?.postMessage({ type: 'aether_key', code, action }, '*'); } catch (_) {} }; window._aetherHeldKeys = window._aetherHeldKeys || new Set(); window._aetherJoyAnalog = { fwd: 0, strafe: 0 }; window._aetherParentDriving = false; const MOVE_KEY_SET = new Set([ 'KeyW','KeyA','KeyS','KeyD', 'ArrowUp','ArrowDown','ArrowLeft','ArrowRight' ]); const ACTION_KEY_SET = new Set(['KeyE','KeyQ','Escape','ShiftLeft','ShiftRight']); const padHeldForWrap = (wrap) => { if (!wrap) return window._aetherHeldKeys; if (!wrap._aetherPadHeld) wrap._aetherPadHeld = new Set(); return wrap._aetherPadHeld; }; const mergedHeldKeys = (wrap) => { const pad = wrap ? padHeldForWrap(wrap) : new Set(); return [...new Set([...window._aetherHeldKeys, ...pad])]; }; window.aetherWalk = (btn, down) => { const wrap = btn?.closest?.('.diorama-wrap'); const frame = wrap?.querySelector('iframe.diorama-frame') || getVisibleDioramaFrame(); if (!frame || !btn) return; window._aetherActiveFrame = frame; const code = btn.dataset?.key; if (!code) return; const padHeld = padHeldForWrap(wrap); if (down) { btn.classList.add('pressed'); padHeld.add(code); } else { btn.classList.remove('pressed'); padHeld.delete(code); } window._aetherParentDriving = true; sendKeysToFrame(frame, mergedHeldKeys(wrap)); }; 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); }); }); }; const wireRemotePadButtons = () => { if (window._aetherPadDelegationBound) return; window._aetherPadDelegationBound = true; const findBtn = (e) => e.target?.closest?.('.drc-btn'); document.addEventListener('pointerdown', (e) => { const btn = findBtn(e); if (!btn) return; e.preventDefault(); btn._aetherPointerId = e.pointerId; try { btn.setPointerCapture(e.pointerId); } catch (_) {} window.aetherWalk(btn, true); }, true); const releaseAllPadButtons = () => { document.querySelectorAll('.drc-btn.pressed').forEach((btn) => { window.aetherWalk(btn, false); }); }; document.addEventListener('pointerup', releaseAllPadButtons, true); document.addEventListener('pointercancel', releaseAllPadButtons, true); document.addEventListener('lostpointercapture', releaseAllPadButtons, true); }; const wireRemoteJoysticks = () => { document.querySelectorAll('.drc-joy').forEach((zone) => { if (zone.dataset.drcJoyBound) return; zone.dataset.drcJoyBound = '1'; const knob = zone.querySelector('.drc-knob'); let active = false, cx = 0, cy = 0; const frameFor = () => zone.closest('.diorama-wrap')?.querySelector('iframe.diorama-frame') || getVisibleDioramaFrame(); const move = (clientX, clientY) => { const dx = clientX - cx, dy = clientY - cy; const dist = Math.min(Math.hypot(dx, dy), 52); const ang = Math.atan2(dy, dx); if (knob) knob.style.transform = `translate(${Math.cos(ang) * dist}px, ${Math.sin(ang) * dist}px)`; const mag = dist > 10 ? 1 : 0; const strafe = mag ? Math.cos(ang) : 0; const fwd = mag ? -Math.sin(ang) : 0; window._aetherJoyAnalog = { fwd, strafe }; const frame = frameFor(); if (frame) { window._aetherActiveFrame = frame; sendAnalogToFrame(frame, fwd, strafe); } }; const end = () => { active = false; if (knob) knob.style.transform = ''; window._aetherJoyAnalog = { fwd: 0, strafe: 0 }; const frame = frameFor(); if (frame) sendAnalogToFrame(frame, 0, 0); }; zone.addEventListener('pointerdown', (e) => { e.preventDefault(); active = true; const r = zone.getBoundingClientRect(); cx = r.left + r.width / 2; cy = r.top + r.height / 2; zone.setPointerCapture(e.pointerId); move(e.clientX, e.clientY); }); zone.addEventListener('pointermove', (e) => { if (!active) return; e.preventDefault(); move(e.clientX, e.clientY); }); zone.addEventListener('pointerup', end); zone.addEventListener('pointercancel', end); zone.addEventListener('pointerleave', end); }); }; wireDioramaFrames(); wireRemotePadButtons(); wireRemoteJoysticks(); new MutationObserver(() => { wireDioramaFrames(); wireRemoteJoysticks(); }).observe(document.body, { childList: true, subtree: true }); 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; return !!el.closest?.('textarea, input, [contenteditable="true"]'); }; const getActiveFrame = () => { const visible = getVisibleDioramaFrame(); if (visible) return visible; const padWrap = document.querySelector('.drc-btn.pressed')?.closest('.diorama-wrap'); if (padWrap) { const padFrame = padWrap.querySelector('iframe.diorama-frame'); if (padFrame) return padFrame; } const registered = window._aetherActiveFrame; if (registered) { const rect = registered.getBoundingClientRect(); if (rect.width > 40 && rect.height > 40) return registered; } return null; }; const forwardKey = (e, action) => { if (!GAME_KEYS.has(e.code)) return; if (typingInForm()) return; const frame = getActiveFrame(); if (!frame) return; e.preventDefault(); e.stopImmediatePropagation(); if (ACTION_KEY_SET.has(e.code)) { if (action === 'down' || e.code === 'ShiftLeft' || e.code === 'ShiftRight') { sendKeyToFrame(frame, e.code, action); } return; } if (!MOVE_KEY_SET.has(e.code)) return; sendKeyToFrame(frame, e.code, action); if (action === 'down') window._aetherHeldKeys.add(e.code); else window._aetherHeldKeys.delete(e.code); window._aetherParentDriving = true; sendKeysToFrame(frame, [...window._aetherHeldKeys]); }; window.addEventListener('keydown', (e) => forwardKey(e, 'down'), true); window.addEventListener('keyup', (e) => forwardKey(e, 'up'), true); // Sync movement to the visible diorama iframe continuously. setInterval(() => { if (typingInForm()) return; const frame = getActiveFrame(); if (!frame) return; const joy = window._aetherJoyAnalog || { fwd: 0, strafe: 0 }; const padWrap = document.querySelector('.drc-btn.pressed')?.closest('.diorama-wrap'); const padHeld = padWrap?._aetherPadHeld; const merged = padHeld ? [...new Set([...window._aetherHeldKeys, ...padHeld])] : [...window._aetherHeldKeys]; const parentActive = merged.length > 0 || Math.abs(joy.fwd) > 0.01 || Math.abs(joy.strafe) > 0.01; if (parentActive) { window._aetherParentDriving = true; if (merged.length) sendKeysToFrame(frame, merged); else sendAnalogToFrame(frame, joy.fwd, joy.strafe); } else if (window._aetherParentDriving) { sendKeysToFrame(frame, []); sendAnalogToFrame(frame, 0, 0); window._aetherParentDriving = false; } }, 50); } 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_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)