# Highlights and clicks

> Outline entities with named styles, publicly or privately per seat, and hear which entity a player clicked - the pick-a-card loop of a scripted game.

- Source: https://diceytable.com/docs/scripting/concepts/highlights-and-clicks
- Last verified: 2026-10-09

A scripted game usually locks its cards and moves them for the players. That keeps the rules in charge, but it
takes away the two things a player does with their hands at a real table: **pointing** ("this one attacks that
one") and **picking** ("I play this"). Highlights and clicks give both back.

- A **highlight** is a coloured outline a table script puts on entities with
  [`world.setHighlight`](https://diceytable.com/docs/scripting-api/world#worldsethighlight). It shows state the rules care about that
  the table cannot show by itself: what is attacking, what is being aimed at, what you could play.
- A **click** is a player pressing and releasing an entity without dragging it. The script hears it through
  [`globalEvents.onObjectClicked`](https://diceytable.com/docs/scripting-api/events#globaleventsonobjectclicked) (or
  [`refObject.onClicked`](https://diceytable.com/docs/scripting-api/objecthandle#objecthandleonclicked)) and decides what it means.

Together they make the loop most card games need: outline what a player may pick, wait for the click, then act
or ask with [`ui.prompt`](https://diceytable.com/docs/scripting-api/game-ui#uiprompt).

Both are for **table scripts only**. A mod has no `api` equivalent of either.

## The three built-in styles

A highlight names a **style**, not a colour, so a game says what an outline means and the platform draws it
the same way everywhere.

| Style | Look | Meaning |
|---|---|---|
| `target` | red-orange, steady | Being aimed at. |
| `active` | gold, **pulsing** | Acting now, for example attacking. |
| `playable` | mint, steady, slightly softer | You could play or use this. |

They were chosen to stay distinct from each other, from the seat colours and from the selection outline, so a
player can read them without a legend. A game can add its own styles, or restyle these three, in the editor;
see [Styles in the editor](https://diceytable.com/docs/scripting/concepts/highlights-and-clicks#styles-in-the-editor).

Outlines work on any entity, face up or face down, on the table, on a card tile in a hand drawer and in VR.

## Public and private outlines

Without options, an outline is **public**: every player sees it. That is right for facts everyone already
knows, such as which unit is attacking which.

With `{ seats: [...] }` it is **private** to those seats, and this matters for hidden information. "The cards in
your hand you can play" says a lot about that hand: drawn publicly, it would tell the opponent what you hold. A
private outline travels like a seat's private [Game UI](https://diceytable.com/docs/scripting/concepts/game-ui#audiences-and-privacy)
state. It is saved with the table, and each seat's outlines reach that seat's browser alone; no other player's
browser ever receives them, not even hidden.

**One outline per entity per audience.** Each entity carries at most one outline from your game publicly and one
per seat privately. Setting a style replaces the one you set before in the same audience, so you never clear
before you set. On screen each player sees **one** outline per entity: a private one beats a public one, and
among several the most recent wins.

## Lifetime

- An outline **stays until you clear it**: [`world.clearHighlights`](https://diceytable.com/docs/scripting-api/world#worldclearhighlights)
  by style or seat, or `world.setHighlight(ids, null)` for particular entities.
- With `seconds` (0.5 to 600) it **removes itself**, on the host's clock, so every player sees it go together.
- Outlines are **saved with the table and restored by undo**, like the rest of your Game UI.
- **Restarting your scripts drops timed outlines only.** Persistent ones stay, so when a script starts, draw the
  outlines again from your own state, as you would your widgets.
- An entity that **leaves the table** (destroyed, drawn, absorbed into a stack) loses its outline.
- The host resolves a style's look **when you call**, so changing a style in the editor affects later calls, not
  outlines already on the table.

Outlines belong to your game's scripts: no other game, Asset pack or mod can change or clear them.

## Styles in the editor

The editor's **HIGHLIGHT STYLES** section, beside **SCENE UI** under the scene scripts, lists the three built-ins
with a swatch and a **Customise** button, and lets you add up to 16 styles of your own (restyled built-ins
included). Each style has a name scripts use, an editor-only label, a colour, a width, an intensity and a pulse.
Styles are scene data, so they follow the top bar's **Changes apply to** scope: each variant can have its own.
See [Highlight Styles](https://diceytable.com/docs/editor/inspector/platform-panels#highlight-styles) for every field.

A script that names a style the game does not have is refused, and the diagnostic lists the names the game does
have.

## What counts as a click

A click is a press and release of the **same** pointer on an entity, without dragging it:

| Input | Counts as a click when |
|---|---|
| Mouse | The pointer moves at most 6 px and is released within 1.5 s. |
| Touch | The finger moves at most 12 px and lifts before the long-press (about 0.45 s) that opens the menu. |
| VR | The trigger is pressed on an entity that cannot be grabbed (for example a locked one), or a grab is a quick, near-motionless tap. |

**Locked entities are clickable**, which is the point: a scripted game locks every card. A tap or click on a
card tile in the player's **hand drawer** counts too.

Clicks are observed beside the table's own pointer handling, never instead of it. Dragging, the right-click or
long-press menu and hover tooltips work exactly as before. A double-click is two clicks. A click with Ctrl,
Shift, Alt or Meta held is not a click, and neither is one made while a tool (ping, draw, stamp, flick) is
active.

## How a click reaches your script

A player's browser sends only "I clicked this entity" to the host, as an `object-click` intent. The host then:

1. drops it if the entity is gone, or if that player has already sent 8 clicks in the last second;
2. looks the sender's **seat** up in its own roster, so
   [`ObjectClickContext.seat`](https://diceytable.com/docs/scripting-api/types#objectclickcontextseat) can be trusted (it is `null` for a
   spectator or a host without a seat);
3. raises the event for your scripts. Nothing on the table changes.

A click is not refused because it is not that player's turn or not their entity. That is deliberate: in a
reaction window the player who must decide is usually **not** the one whose turn it is. Your handler decides who
may click what, by checking `context.seat` and whether the entity is one you offered.

## The reaction-window pattern

This is the loop a scripted card game uses when one player attacks and another may answer:

1. Outline the attacker `active` and what it attacks `target`, publicly.
2. Work out which cards the defending seat could play, and outline them `playable` with `{ seats: [defender] }`.
3. In `onObjectClicked`, ignore every click that is not from the defending seat or not on one of those cards.
4. For a click that is, ask the defender with `ui.prompt(seat, { title, actions })`: the card's options, plus a
   way back.
5. When the window closes (a choice, a pass or a timeout), `world.clearHighlights({ style: "playable" })`, or
   clear everything when the attack resolves.

```ts
// content/scripting-api/examples/concepts.highlights-and-clicks.ts

// Scene script: an attack with a reaction phase, start to finish.
//
// 1. The attacker and its target get PUBLIC outlines.
// 2. The defending seat's playable cards get a PRIVATE outline.
// 3. A click on one of them asks the defender what to do with it.
// 4. The reaction phase closes after a choice, or after 20 seconds, and the
//    outlines go.

interface Attack {
  attackerId: string;
  targetId: string;
  defender: string;
  playable: Set<string>;
}

let attack: Attack | null = null;

// Your rules decide what a seat may play. Here: every card in hand.
function playableFor(handIds: readonly string[]): string[] {
  return [...handIds];
}

function openAttack(attackerId: string, targetId: string, defender: string, handIds: readonly string[]): void {
  const playable = playableFor(handIds);
  const current: Attack = { attackerId, targetId, defender, playable: new Set(playable) };
  attack = current;
  world.setHighlight(attackerId, "active");
  world.setHighlight(targetId, "target");
  if (playable.length > 0) {
    world.setHighlight(playable, "playable", { seats: [defender] });
  }
  void world.wait(20).then(() => {
    if (attack === current) {
      resolveAttack(null);
    }
  });
}

function resolveAttack(reactionCardId: string | null): void {
  const current = attack;
  if (current === null) {
    return;
  }
  attack = null;
  // Every outline this game set: public, and every seat's private ones.
  world.clearHighlights();
  world.broadcast(
    reactionCardId === null ? `${current.defender} does not react.` : `${current.defender} reacts.`
  );
}

globalEvents.onObjectClicked.add((card, context) => {
  const current = attack;
  if (current === null || context.seat !== current.defender || !current.playable.has(card.id)) {
    return;
  }
  ui.prompt(current.defender, {
    title: card.name ?? "React with this card?",
    actions: [
      { id: "play", label: "Play it" },
      { id: "back", label: "Choose another" }
    ],
    timeoutSeconds: 15
  })
    .then((result) => {
      if (attack === current && result.actionId === "play") {
        resolveAttack(card.id);
      }
    })
    .catch((error: unknown) => world.log(`Could not ask ${current.defender}: ${String(error)}`));
});

void openAttack;
```

## Limits

- **Table scripts only.** A mod has no `api` call for outlines and no `api.on` hook for clicks.
- **Not in the Edit Mode viewport.** The editor's viewport uses its own transform tools, so a click there selects
  instead. Play-test the game to try click handlers and see outlines in play.
- **One outline per entity per viewer.** A player never sees two at once; plan styles so the most important one
  is the one you set last, or keep audiences apart.
- **Caps.** At most 256 entities per `setHighlight` call and 256 outlined entities per audience; at most 16 styles
  per game. Over a cap the call is refused with a diagnostic.
- **8 clicks per second per player.** The host drops the rest.

## See also

- [`world.setHighlight`](https://diceytable.com/docs/scripting-api/world#worldsethighlight) and [`world.clearHighlights`](https://diceytable.com/docs/scripting-api/world#worldclearhighlights) — the reference.
- [`globalEvents.onObjectClicked`](https://diceytable.com/docs/scripting-api/events#globaleventsonobjectclicked) and [`ObjectClickContext`](https://diceytable.com/docs/scripting-api/types#objectclickcontext) — the click event.
- [Game UI (HUD)](https://diceytable.com/docs/scripting/concepts/game-ui) — prompts, private state, and the other half of a scripted game's interface.
- [Highlight Styles](https://diceytable.com/docs/editor/inspector/platform-panels#highlight-styles) — the editor section.
- [Host authority](https://diceytable.com/docs/scripting/concepts/host-authority) — why both run on the host.
