diff --git "a/app.py" "b/app.py"
--- "a/app.py"
+++ "b/app.py"
@@ -31,15 +31,8 @@ from world.locations import get_all_locations, get_location_by_id, update_entity
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.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,
@@ -53,7 +46,7 @@ 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.relationship_graph import render_relationship_graph
from ui.share import build_soul_card, share_caption
CSS_PATH = Path(__file__).parent / "ui" / "styles.css"
@@ -78,108 +71,21 @@ _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 cover intro and table-of-contents overlay."""
+ return """
'
- '
Click a glowing place on the map to unveil its secrets.
'
+ '
'
+ '
Choose a glowing place on the map
'
+ '
Tap any marker to open that location. '
+ 'On phone, you will jump straight to the Explore spread so you can enter immediately.
'
'
'
)
location = get_location_by_id(location_id)
if not location:
- return '
'
+ return ""
- name = _esc(location["name"])
- desc = _esc(location.get("short_description", ""))
- return (
- '
'
- f'
'
- f'✦ '
- f'{name} '
- f' — {desc} '
- f'
'
+ 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)
@@ -333,8 +229,7 @@ def _summon_error(message: str):
render_location_panel(None),
_HIDE_CARD,
_HIDE_CAPTION,
- render_realm_pulse(),
- render_realm_stats(),
+ "",
)
@@ -375,8 +270,7 @@ def summon_entity(description: str, session_id: str):
render_location_panel(entity["location_id"]),
card_update,
caption_update,
- render_realm_pulse(),
- render_realm_stats(),
+ render_diorama(entity["location_id"]),
)
@@ -409,50 +303,11 @@ def filter_entities(location_filter, type_filter, status_filter, search):
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)
+ return render_entity_grid(entities)
-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 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:
@@ -628,48 +483,28 @@ def refresh_all_views():
_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))
+ return None, render_diorama(None), render_room(None), gr.update(value=[]), [], _SOUL_HINT
loc_id = ids[evt.index]
- return (loc_id, *_explore_scene(loc_id))
+ 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, *_explore_scene(None))
- return (loc_id, *_explore_scene(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):
- try:
- loc_id = int(location_id) if location_id else None
- except (TypeError, ValueError):
- loc_id = None
- return _explore_scene(loc_id)
+ 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):
@@ -690,143 +525,87 @@ def build_app() -> gr.Blocks:
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.
'
- '
'
- '⟡ '
- ' '
- 'Map of the Canopy '
- ' '
- '⟡ '
- '
'
- )
- 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.
'
- '
'
+ 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",
)
- 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.
'
+ location_panel = gr.HTML(
+ value=render_location_panel(None),
)
- live_tick_btn = gr.Button("Run simulation tick →", variant="secondary", size="sm")
- tick_result = gr.HTML()
+ 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()
location_select = gr.Dropdown(
choices=[(l["name"], l["id"]) for l in get_all_locations()],
label="Explore Location",
- visible=False,
+ visible=True,
)
+ featured_chars = gr.HTML(value=render_featured_characters())
+
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(
- ''
- )
-
- 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"],
- )
+ 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=[],
@@ -840,193 +619,100 @@ def build_app() -> gr.Blocks:
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.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 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())
+ 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("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.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",
+ )
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('
❧
')
+ 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=5,
+ scale=4,
+ )
+ summon_btn = gr.Button("Seal & Summon", variant="primary", scale=1, elem_classes=["seal-summon-btn"])
+
+ 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,
+ )
_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,
+ soul_card_img, share_caption_box, realm_diorama,
]
summon_btn.click(
@@ -1048,7 +734,8 @@ def build_app() -> gr.Blocks:
() => {
setTimeout(() => {
const card = document.querySelector('.soul-card-image');
- const target = card || document.querySelector('.tome-summon-stage .entity-card');
+ const diorama = document.querySelector('.diorama-wrap');
+ const target = card || diorama || document.querySelector('.entity-card');
if (target) target.scrollIntoView({ behavior: 'smooth', block: 'center' });
}, 350);
}
@@ -1074,56 +761,55 @@ def build_app() -> gr.Blocks:
() => {
setTimeout(() => {
const card = document.querySelector('.soul-card-image');
- const target = card || document.querySelector('.tome-summon-stage .entity-card');
+ 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_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)
+ 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_map_pick,
+ fn=_realm_location_change,
inputs=[location_select],
- outputs=[world_map, location_panel],
+ 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(
- _realm_map_pick,
+ _map_open_diorama,
inputs=[map_pick],
- outputs=[world_map, location_panel],
- )
-
- explore_place_pick.change(
- open_diorama_from_map,
- inputs=[explore_place_pick],
- outputs=_explore_outputs,
+ 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=[explore_place_pick],
+ inputs=[map_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,
+ 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],
@@ -1133,12 +819,12 @@ def build_app() -> gr.Blocks:
places_gallery.select(
enter_place,
inputs=None,
- outputs=[explore_loc, *_explore_outputs],
+ 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, *_explore_outputs],
+ 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])
@@ -1149,7 +835,7 @@ def build_app() -> gr.Blocks:
).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")
@@ -1201,89 +887,34 @@ def build_app() -> gr.Blocks:
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],
- )
+ 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_detail],
+ [entity_grid],
)
- entity_pick.change(select_entity_by_id, [entity_pick], [entity_detail])
+ 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]).then(
- fn=render_bonds_sidebar, outputs=[bonds_sidebar]
- ).then(fn=render_realm_stats, outputs=[realm_stats])
+ ).then(fn=render_relationship_graph, outputs=[rel_graph])
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 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: 'bonds', label: 'Bonds and Alliances', kind: 'tab', panel: 3 },
{ key: 'summon', label: 'Summon a New Soul', kind: 'tab', panel: 5 }
];
let currentSpread = 0;
@@ -1305,13 +936,11 @@ def build_app() -> gr.Blocks:
panels.forEach((panel, i) => {
const active = i === panelIndex;
panel.style.setProperty('display', active ? 'flex' : 'none', 'important');
- panel.style.setProperty('flex-direction', 'column', 'important');
panel.style.setProperty('height', active ? '565px' : '0', 'important');
panel.style.setProperty('max-height', active ? '565px' : '0', 'important');
- panel.style.setProperty('overflow', 'hidden', 'important');
+ panel.style.setProperty('overflow', active ? 'auto' : 'hidden', 'important');
panel.setAttribute('aria-hidden', active ? 'false' : 'true');
});
- wirePageExpand();
const pad = document.getElementById('aether-walk-pad');
if (pad && panelIndex !== 1) {
pad.classList.remove('visible', 'explore-only');
@@ -1319,126 +948,37 @@ def build_app() -> gr.Blocks:
};
const ensureAmbientMelody = () => {
- if (window._aetherAmbientStarted) {
- window._aetherAmbientCtx?.resume?.();
- return;
- }
+ if (window._aetherAmbientStarted) 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.gain.value = 0.022;
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 notes = [220, 261.63, 329.63];
+ notes.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';
+ const gain = ctx.createGain();
+ osc.type = i === 0 ? 'sine' : '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);
+ gain.gain.value = 0.0001;
+ osc.connect(gain);
+ gain.connect(master);
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);
+
+ const pulse = () => {
+ const now = ctx.currentTime;
+ gain.gain.cancelScheduledValues(now);
+ gain.gain.setValueAtTime(Math.max(gain.gain.value, 0.0001), now);
+ gain.gain.exponentialRampToValueAtTime(0.014 / (i + 1), now + 2.2 + i * 0.35);
+ gain.gain.exponentialRampToValueAtTime(0.00012, now + 7.5 + i * 0.4);
};
- breathe();
- setInterval(breathe, 9000 + i * 800);
+ pulse();
+ setInterval(pulse, 7000 + i * 400);
});
-
- 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 (_) {}
};
@@ -1477,7 +1017,7 @@ def build_app() -> gr.Blocks:
const page = document.getElementById('tome-page-indicator');
if (!page) return;
const idx = Math.max(1, currentSpread + 1);
- page.textContent = `Page ${toRoman(idx)}`;
+ page.textContent = `Page ${toRoman(idx + 4)}`;
};
const closeToc = () => {
@@ -1487,48 +1027,7 @@ def build_app() -> gr.Blocks:
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;
@@ -1560,13 +1059,13 @@ def build_app() -> gr.Blocks:
❮
❯
Table of Contents
-
Page I
+
Page V
- ${sections.map((s, i) => `${toRoman(i + 1)}. ${s.label} `).join('')}
+ ${sections.map((s, i) => `${s.key === 'summon' ? 'VI. Summon a New Soul' : `${toRoman(i + 1)}. ${s.label}`} `).join('')}
Jump to section
- ${sections.map((s, i) => `${toRoman(i + 1)}. ${s.label} `).join('')}
+ ${sections.map((s, i) => `${s.key === 'summon' ? 'VI. Summon a New Soul' : `${toRoman(i + 1)}. ${s.label}`} `).join('')}
`;
document.body.appendChild(nav);
@@ -1583,13 +1082,7 @@ def build_app() -> gr.Blocks:
}
if (next && !next.dataset.bound) {
next.dataset.bound = '1';
- next.addEventListener('click', () => {
- if (currentSpread >= sections.length - 1) {
- closeBook();
- return;
- }
- goToSpread(currentSpread + 1, 'next');
- });
+ next.addEventListener('click', () => goToSpread(currentSpread + 1, 'next'));
}
if (tocOpen && !tocOpen.dataset.bound) {
tocOpen.dataset.bound = '1';
@@ -1620,10 +1113,7 @@ def build_app() -> gr.Blocks:
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 === 'ArrowRight') goToSpread(currentSpread + 1, 'next');
if (e.key === 'ArrowLeft') goToSpread(currentSpread - 1, 'prev');
if (e.key === 'Escape') closeToc();
}, { passive: true });
@@ -1633,465 +1123,67 @@ def build_app() -> gr.Blocks:
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 btn = document.getElementById('realm-opening-enter');
+ if (btn) {
+ btn.addEventListener('click', () => {
+ playOpeningChime();
+ ensureAmbientMelody();
+ gate.classList.add('is-opened');
+ closeToc();
+ setTimeout(() => {
+ gate.classList.add('is-closed');
+ }, 450);
});
}
- 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();
- 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) => {
+ document.querySelectorAll('#world-map [data-location-id]').forEach((node) => {
+ if (node.dataset.mapBound) return;
+ node.dataset.mapBound = '1';
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);
- });
- });
- 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();
+ const setHiddenValue = (rootId, value) => {
+ const root = document.querySelector(`#${rootId}`);
+ const input = root && (root.querySelector('textarea') || root.querySelector('input'));
+ if (!input || !value) return;
+ input.value = value;
+ input.dispatchEvent(new Event('input', { bubbles: true }));
+ };
+ node.addEventListener('click', () => {
+ const id = node.getAttribute('data-location-id');
+ if (!id) return;
+
+ setHiddenValue('map_location_pick', id);
+ setHiddenValue('portal_location_pick', id);
+
+ const isPhoneLayout = window.matchMedia('(max-width: 920px)').matches;
+ if (isPhoneLayout) {
+ goToSpread(1, 'next');
+ setTimeout(() => {
+ const target = document.querySelector('iframe.diorama-frame')
+ || document.querySelector('.room-view')
+ || document.querySelector('.diorama-empty');
+ if (target) {
+ target.scrollIntoView({
+ behavior: prefersReduced ? 'auto' : 'smooth',
+ block: 'start'
+ });
+ }
+ }, 360);
+ }
});
});
};
-
bind();
- bindSpreadClicks();
- new MutationObserver(() => {
- bind();
- bindSpreadClicks();
- }).observe(document.body, { childList: true, subtree: true });
+ new MutationObserver(bind).observe(document.body, { childList: true, subtree: true });
window._aetherHoverDiorama = false;
window._aetherDioramaActive = false;
@@ -2155,37 +1247,29 @@ def build_app() -> gr.Blocks:
};
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 = `
-
-
- W
-
- A
- S
- D
-
`;
- }
- const stage = getVisibleDioramaFrame()?.closest('.diorama-stage');
- if (stage && el.parentElement !== stage) {
- stage.appendChild(el);
- }
+ if (document.getElementById('aether-walk-pad')) return;
+ const el = document.createElement('div');
+ el.id = 'aether-walk-pad';
+ el.setAttribute('aria-label', 'Walk');
+ el.innerHTML = `
+
+
+ W
+
+ A
+ S
+ D
+
`;
+ document.body.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';
+ const show = !!frame && !pad.classList.contains('controls-hidden') && window._aetherCurrentSpread === 'explore';
pad.classList.toggle('visible', show);
- pad.classList.toggle('explore-only', show);
+ pad.classList.toggle('explore-only', show);
};
const wireWalkPad = () => {
@@ -2261,7 +1345,6 @@ def build_app() -> gr.Blocks:
wireDioramaFrames();
wireWalkPad();
updateWalkPadVisibility();
- wirePageExpand();
}).observe(document.body, { childList: true, subtree: true });
setInterval(updateWalkPadVisibility, 800);