| """Entity profile HTML component.""" |
|
|
| from __future__ import annotations |
|
|
| from ui import assets |
| from world.events import get_interactions_for_entity |
| from world.locations import get_location_by_id |
| from world.relationships import get_relationships_enriched |
| from ui.avatars import render_avatar |
|
|
| REL_LABELS = { |
| "allied": "Allied", |
| "friends": "Friends", |
| "rivals": "Rivals", |
| "enemies": "Enemies", |
| "mentor": "Mentor", |
| "owes_debt": "Owes a debt", |
| } |
|
|
|
|
| TYPE_LABELS = { |
| "character": "Character", |
| "creature": "Creature", |
| "object": "Object", |
| "place": "Place", |
| } |
|
|
| STATUS_LABELS = { |
| "active": "Active", |
| "dormant": "Dormant", |
| "legendary": "Legendary", |
| } |
|
|
|
|
| def render_entity_card(entity: dict) -> str: |
| location = get_location_by_id(entity["location_id"]) |
| loc_name = location["name"].replace("The ", "") if location else "Unknown" |
| type_label = TYPE_LABELS.get(entity["type"], entity["type"].title()) |
| status = STATUS_LABELS.get(entity["status"], entity["status"].title()) |
| status_class = f"status-{entity['status']}" |
|
|
| traits_html = "".join( |
| f'<span class="trait-tag">{t}</span>' for t in entity["personality_traits"] |
| ) |
|
|
| tags_html = "" |
| if entity.get("tags"): |
| tags_html = "".join( |
| f'<span class="entity-tag">{t}</span>' for t in entity["tags"] |
| ) |
|
|
| secret_html = "" |
| if entity.get("secret_name"): |
| secret_html = f""" |
| <div class="entity-section secret-section"> |
| <div class="section-label">Secret Name <span class="sea-whisper">(only the Sea knows)</span></div> |
| <p class="secret-name">{entity['secret_name']}</p> |
| </div> |
| """ |
|
|
| memory = entity["memory_summary"] or "Nothing yet — they just arrived." |
| rel_count = entity.get("relationship_count", 0) |
| int_count = entity.get("interaction_count", 0) |
|
|
| rels = get_relationships_enriched(entity["id"]) |
| rel_html = "" |
| if rels: |
| items = [] |
| for r in rels[:8]: |
| label = REL_LABELS.get(r["relationship_type"], r["relationship_type"].title()) |
| items.append( |
| f'<li class="bond-item">' |
| f'<span class="bond-partner">{r["partner_name"]}</span> ' |
| f'<span class="bond-type">{label}</span> ' |
| f'<span class="bond-strength">{"●" * r["strength"]}</span>' |
| f'<p class="bond-desc">{r["description"]}</p></li>' |
| ) |
| rel_html = f""" |
| <div class="entity-section bonds-section"> |
| <div class="section-label">Bonds in the Realm</div> |
| <ul class="bond-list">{"".join(items)}</ul> |
| </div> |
| """ |
|
|
| encounters = get_interactions_for_entity(entity["id"], limit=5) |
| enc_html = "" |
| if encounters: |
| enc_items = [] |
| for ix in encounters: |
| other = ( |
| ix["entity_b_name"] |
| if ix["entity_a_id"] == entity["id"] |
| else ix["entity_a_name"] |
| ) |
| loc = (ix.get("location_name") or "").replace("The ", "") |
| enc_items.append( |
| f'<li class="encounter-item">' |
| f'<span class="enc-day">Day {ix["world_day"]}</span> ' |
| f'Met <b>{other}</b> at {loc} — ' |
| f'<em>{ix.get("book_of_ages_entry") or ix["description"][:100]}</em></li>' |
| ) |
| enc_html = f""" |
| <div class="entity-section encounters-section"> |
| <div class="section-label">Encounters Remembered</div> |
| <ul class="encounter-list">{"".join(enc_items)}</ul> |
| </div> |
| """ |
|
|
| slug = location["slug"] if location else None |
| avatar = render_avatar(entity, size=96, location_slug=slug) |
| loc_img = assets.location_image_url(slug) |
| banner_style = f"background-image: url('{loc_img}');" if loc_img else "" |
|
|
| return f""" |
| <div class="entity-card"> |
| <div class="entity-card-banner" style="{banner_style}"> |
| <div class="entity-card-banner-scrim"></div> |
| </div> |
| <div class="entity-card-top"> |
| <div class="entity-portrait">{avatar}</div> |
| <div class="entity-header"> |
| <div class="entity-meta"> |
| <span class="entity-type">{type_label} ▸ {loc_name}</span> |
| <span class="entity-status {status_class}">Day {entity['days_in_realm']} · {status}</span> |
| </div> |
| <h2 class="entity-name">{entity['name']}</h2> |
| {f'<div class="entity-tags">{tags_html}</div>' if tags_html else ''} |
| </div> |
| </div> |
| |
| <div class="entity-appearance">{entity['appearance']}</div> |
| |
| <div class="entity-traits">{traits_html}</div> |
| |
| <div class="entity-section"> |
| <div class="section-label">Seeks</div> |
| <p>{entity['primary_goal']}</p> |
| </div> |
| |
| <div class="entity-section secret-goal"> |
| <div class="section-label">Secretly</div> |
| <p>{entity['secondary_goal']}</p> |
| </div> |
| |
| <div class="entity-section"> |
| <div class="section-label">Fears</div> |
| <p>{entity['primary_fear']}</p> |
| </div> |
| |
| <blockquote class="entity-greeting">"{entity['greeting']}"</blockquote> |
| |
| {secret_html} |
| |
| <div class="entity-section memory-section"> |
| <div class="section-label">What They Remember</div> |
| <p class="memory-text">{memory}</p> |
| </div> |
| |
| {rel_html} |
| {enc_html} |
| |
| <div class="entity-footer"> |
| {rel_count} relationships · {int_count} encounters |
| </div> |
| </div> |
| """ |
|
|
|
|
| def render_entity_hologram(entity: dict | None) -> str: |
| """3D-styled entity detail panel for the Souls spread right page.""" |
| if not entity: |
| return """ |
| <div class="entity-hologram entity-hologram-empty"> |
| <div class="hologram-stage"> |
| <p class="hologram-hint">Select a soul on the left leaf to unveil their story.</p> |
| </div> |
| </div> |
| """ |
|
|
| location = get_location_by_id(entity["location_id"]) |
| loc_name = location["name"].replace("The ", "") if location else "Unknown" |
| slug = location["slug"] if location else None |
| avatar = render_avatar(entity, size=72, location_slug=slug) |
| loc_img = assets.location_image_url(slug) |
| banner = f"background-image:url('{loc_img}');" if loc_img else "" |
| type_label = TYPE_LABELS.get(entity["type"], entity["type"].title()) |
| status = STATUS_LABELS.get(entity["status"], entity["status"].title()) |
|
|
| return f""" |
| <div class="entity-hologram" data-entity-id="{entity['id']}"> |
| <div class="hologram-beam" aria-hidden="true"></div> |
| <div class="hologram-stage"> |
| <div class="hologram-banner" style="{banner}"> |
| <div class="hologram-banner-scrim"></div> |
| </div> |
| <div class="hologram-scroll"> |
| <div class="hologram-identity-row"> |
| <div class="hologram-portrait-wrap">{avatar}</div> |
| <div class="hologram-identity-text"> |
| <p class="hologram-meta">{type_label} · {loc_name} · Day {entity['days_in_realm']} · {status}</p> |
| <h2 class="hologram-name">{entity['display_name']}</h2> |
| </div> |
| </div> |
| <p class="hologram-appearance">{entity['appearance']}</p> |
| <div class="hologram-goals"> |
| <p><b>Seeks</b> {entity['primary_goal']}</p> |
| <p><b>Fears</b> {entity['primary_fear']}</p> |
| </div> |
| <blockquote class="hologram-greeting">"{entity['greeting']}"</blockquote> |
| <p class="hologram-memory">{entity['memory_summary'] or 'They have only just arrived.'}</p> |
| </div> |
| </div> |
| </div> |
| """ |
|
|
|
|
| def render_entity_grid(entities: list[dict]) -> str: |
| if not entities: |
| return '<div class="empty-state">The Realm waits for its first arrivals.</div>' |
|
|
| cards = [] |
| for entity in entities: |
| location = get_location_by_id(entity["location_id"]) |
| loc_short = location["name"].replace("The ", "")[:20] if location else "?" |
| status_class = f"grid-status-{entity['status']}" |
|
|
| slug = location["slug"] if location else None |
| avatar = render_avatar(entity, size=48, location_slug=slug) |
|
|
| cards.append(f""" |
| <div class="entity-grid-card {status_class}" data-entity-id="{entity['id']}"> |
| <div class="grid-card-portrait">{avatar}</div> |
| <div class="grid-card-body"> |
| <div class="grid-card-type">{entity['type']}</div> |
| <h3 class="grid-card-name">{entity['display_name']}</h3> |
| <p class="grid-card-location">{loc_short}</p> |
| <p class="grid-card-preview">{entity['appearance'][:120]}{'…' if len(entity['appearance']) > 120 else ''}</p> |
| <div class="grid-card-meta">Day {entity['days_in_realm']} · {entity['status']}</div> |
| </div> |
| </div> |
| """) |
|
|
| return f'<div class="entity-grid">{"".join(cards)}</div>' |
|
|