aether-garden / ui /entity_card.py
kavyabhand's picture
Deploy Aether Garden application
f004bea verified
Raw
History Blame Contribute Delete
9.04 kB
"""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>'