"""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['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)