Skip to content

Overlays — cards, HUDs & cutscenes on top of the view

An overlay is a screen-anchored surface drawn on top of a console's page and its embedded engine views (the 3D view, the tactical map). Hero cards, lower thirds, banners, a full-screen flash, a modal choice, a live HUD — all draw over whatever is on screen and update without repainting the page underneath.

The corner toast is retired

overlay_toast() no longer draws anything — it writes to the ship's log (messages). It was the one surface carrying information with no durable record: it said its piece and took it with it, so a console that connected a second later never saw it. The call still works, and so does the toast <text> quest directive; icon, seconds and slot are accepted and ignored.

Overlays live in named slots (centre, top strip, corner, bottom, full-screen), and each slot draws above the page via the engine's draw-layer. You never build the stacking or the region plumbing — you call a wrapper (or fire a signal), and the overlay system establishes the slot, draws it, and clears it.

Quick start — a hero card

overlay_hero("CHAPTER TWO", subtitle="The Long Dark", seconds=4)

A big centred card that lifts itself after four seconds. Give it a visual above the title — a face, a ship, an icon, or an image (first one set wins):

overlay_hero("Admiral Harkin", subtitle="Hold the line, commander.",
             face=get_face(admiral.id))

Text that doesn't fit — timed parts, not a clipped line

A banner strip and a lower third are one line wide. Clamping a long message keeps it readable but throws away the tail, which for an alert is usually the part that matters. So text that will not fit is measured, split into parts that each fit, and played in sequence:

overlay_banner("Long range sensors report a raider wing crossing the neutral zone. "
               "All ships to red alert; hold station until the escort arrives.")
  • The measurement is the engine's (the same primitive content-sizing uses), and it is done per client — whether it fits depends on that screen.
  • Dwell is paced by length (~2.6 words/second, clamped to 2.5–7s); override with dwell=.
  • A sticky banner (no seconds) loops; one with a lifetime plays through once and clears. A lower third never loops — a repeating subtitle reads as a stutter.
  • The cycle is generation-guarded: a newer banner, or a clear, stops it rather than fighting for the strip.
  • cycle=False restores the old spill/clip behaviour, and if the engine cannot measure (headless, no client yet) the text is left whole.

announce() uses this: alert and hail hand their full text to the slot, because it can play the whole thing. Only a hero card still gets a clamped headline — it is a glance, with nowhere for a paragraph to go.

Legibility — scrims, fills and framing

An overlay draws over a live 3D view, so white text on nothing is often unreadable. Two knobs, both translucent-friendly:

  • background lays a colour under the card (overlay_hero) or fills the strip (overlay_banner, filled #000a by default — pass None for bare text).
  • letterbox=True on a hero also drops cinematic bars on the full-screen slot, so one call gives a framed title card; pass a string to use it as the line between the bars, and the bars lift with the card when seconds is set.
overlay_hero("CHAPTER TWO", subtitle="The Long Dark",
             background="#000a", letterbox="Approaching the anomaly", seconds=6)
overlay_banner("RED ALERT")                       # filled strip
overlay_banner("SUBTLE", background=None)         # bare text on the view

The built-in overlays

Each wrapper packs your arguments into content and draws the matching card. All the transient ones take an optional seconds (auto-dismiss) and a to (which consoles — see Targeting).

Wrapper Slot Use
overlay_hero(title, subtitle, face/ship/icon/image, background, letterbox, seconds) centre chapter / scene title, boss reveal
overlay_lower_third(name, line) bottom someone speaking over the live view
overlay_banner(text, color) top strip RED ALERT, a countdown
overlay_credits(entries, title, roll=) full-screen opening / closing credits
overlay_letterbox(line, bar) full-screen cinematic bars for a cutscene
overlay_flash(color) full-screen hull-hit / jump colour wash (fast)
overlay_choice(title, buttons) centre modal — returns an awaitable
overlay_hud(rows, controls, title) anchored sticky live readout + controls
overlay_clear(slot=None) clear one slot, or all

Targeting — which consoles

Overlays draw on consoles, but you usually hold a ship or a side. to is an audience expression that accepts any of them:

to goes to
(omitted) the current console (the one whose task called it)
a client id that one console
a ship (id or object) every console linked to that ship
a side (key string or side agent) every console of every ship on that side
a role set / list the union, elementwise
overlay_hero("FLEET ALERT", subtitle="Raiders inbound", to=role("mainscreen"))
overlay_banner("WAR DECLARED", to=role("__player__"))      # ships -> their consoles
overlay_banner("BLOCKADE LIFTED", to="tsn")                # a whole side

A mixed set is fine — ships and clients resolve side by side, and anything that isn't a console is skipped. Resolve it yourself with consoles_of(to).

consoles= narrows by console role. A ship has a whole bridge; often you mean one screen:

overlay_lower_third(name, line, to=ship_id, consoles="mainscreen")
overlay_banner("Contact bearing 040", to=ship_id, consoles="science, comms")

Passing a scalar to that resolves to no console logs a one-off warning (that is the "I pushed an overlay and saw nothing" bug); an empty set is normal and stays quiet.

Auto-dismiss

seconds schedules a generation-guarded auto-dismiss: if the slot is re-shown or updated before the timer fires, the old timer is superseded — it can never clear the newer content.

An overlay slot holds one thing: showing again replaces what is there. Stacking used to be the corner toast's trick, and it retired with it — several notifications that all want keeping are a log, not a pile of cards (messages).

A modal choice

overlay_choice returns an awaitable that resolves to the pressed button's label. Await it from a story / background task (not the target console's own GUI task):

result = await overlay_choice("Fire on the ambassador?", ["Yes", "No"], to=player)
if result.data == "Yes":
    open_fire()

A live HUD

overlay_hud shows a sticky panel over the live view; overlay_hud_update re-fills just that region (no page repaint). Update only when a shown value changes:

overlay_hud(rows={"Speed": 0, "Alert": "GREEN"}, title="SHIP HUD",
            controls=[{"label": "Toggle Alert", "action": alert_toggle_label,
                       "data": {"ship": ship_id}}], to=console)

# a watcher sub-task, once a second, when the value moves:
overlay_hud_update(rows={"Speed": speed, "Alert": alert}, to=console)

Rows accept a dict or a list of (label, value) pairs. A control's action is a MAST label run as a sub-task (so a toggle never hijacks the console's own GUI).

Fire an overlay with a signal

There is no auto-wired signal handler, but a mission drops in a one-line //shared/signal forwarder (one dispatch on the server fans out to the to targets):

//shared/signal/overlay
    overlay_signal_show(to, slot, kind, fields)
signal_emit("overlay", {"to": role("mainscreen"), "slot": "center_hero",
                        "kind": "hero", "fields": {"title": "CHAPTER TWO"}})

Declarative overlays in AMD

Author overlays as data in an .amd file — a projection of the AMD document form (see AMD tools) — and fire them by key. The fence fields become content; the body is the kind's main text; Seconds auto-dismisses.

## [Chapter Two](ch2)
---
Kind: hero
Subtitle: The Long Dark
Seconds: 4
---
CHAPTER TWO
doc = document_get_amd_file(get_mission_dir_filename("overlays.amd"))
amd_overlays(amd_section(doc, "overlays"))
overlay_amd("ch2", to=role("mainscreen"))

Quests fire overlays

A quest fires overlays at accept / complete / fail, to the quest's participant consoles — no wiring. Two forms, both work (underscore or spaced keys):

## [Rescue the Convoy](rescue)
---
Complete Overlay: convoy_saved        # reference a declared amd_overlays record
On complete: hero CONVOY SAVED         # or an inline <kind> <text>
On fail: banner Convoy lost
---
Escort the convoy to the jump point.

Custom cards — your own kind

A kind maps to a builder. Author one in Python or in MAST.

MAST (the sugar): a //overlay/<kind> route builds the card with gui_* verbs; content fields arrive as task variables. It registers itself at load — no Python, no registration call:

//overlay/briefing
    gui_row("row-height: 8em;")
    gui_face(face)
    gui_row("row-height: content;")
    gui_text(f"$text:`{name}`;justify:center;font:gui-5")
    gui_row("row-height: content;")
    gui_text(f"$text:`{line}`;justify:center;font:gui-3")
overlay_show("center_hero", "briefing", name="Harkin", line="Hold the line.",
             face=get_face(admiral.id))

Python: register a function for full control (or library builders):

def _briefing(cid, content):
    gui_row("row-height: 8em;"); gui_face(content["face"])
    gui_row("row-height: content;"); gui_text(f"$text:`{content['name']}`;")

overlay_register("briefing", _briefing)

Either way, the builder decides layout; the content decides what shows — the same card can be driven from a wrapper, a signal, AMD, or a quest.

announce() — the overlay AND the record, in one call

An overlay is an attention layer: it draws over the view, then it is gone. It keeps no history, and a console that connects a second later never saw it. So the house rule is that an overlay never carries information alone — anything a player may need to act on later gets a durable twin.

announce() does both halves, picked by level:

level overlay durable twin
chapter hero card info-panel card (history)
hail lower third comms_message from sender (else a card)
alert top banner info-panel card (history)
status none a line in the ship's log
minor none a line in the ship's log

status and minor draw no overlay. They were the one pair carrying information on a surface that kept no record; they are log lines now — visible immediately in the strip where the toast used to appear, and still there when the crew looks back. Pass record=True to force a card instead, exactly as before.

announce("Raiders have crossed the line.", title="TSN Command",
         level="alert", to="tsn")

announce(line, title=admiral.name, face=get_face(admiral.id),
         level="hail", sender=admiral.id, ship=artemis_id)

The overlay gets a headlineannounce_headline() folds it to one ASCII line and clamps it (engine text is ASCII-only, and a card is a glance). The full text goes to the twin. record=False suppresses the twin when it is already being sent another way; record=True forces a card on a level that has none.

The info panel is the log, not the popup

Overlays took the attention job, so the info panel leans into the one thing only it can do: keep the record. A card sent with comms_info_card / gui_info_panel_send_message is filed in its tab's log and readable any time on the log tab -- it does not take over the panel.

A card interrupts (shows live, switches the panel to its tab, auto-dismisses) in exactly two cases:

interrupts?
card with a button always -- it is a progression gate; a mission awaiting the press deadlocks if it is never seen
notify=True yes -- for a card that genuinely must be read now
anything else no -- filed in the log; pair it with an overlay for the attention half
comms_info_card(consoles, text, title="Command")                # filed, quiet
comms_info_card(consoles, text, title="Command", notify=True)   # interrupts
announce(text, title="Command", level="alert", to=ship)         # banner + filed card

announce() is the front door that gets this right by construction: the overlay interrupts, the card records. The log keeps 50 cards (INFO_PANEL_LOG_MAX).

How it draws (the one rule)

Overlays keep off the page's repaint path: each slot is its own sub-region, established during the page's repaint and then updated out-of-band (clear → fill → complete on just that region). A slot's builder is re-run every repaint, so keep custom builders build-only — no await, no state changes, just gui_*.

Draw order is by slot (full-screen over centre over banner over the page). Overlays draw fine over the 3D view; controls placed over the interactive 2D map are a pending engine capability.