GameTrophy

Methods

.effectCallback({ scene })
.unlock()
.define(tag, registry)
.subscribe(context, callback)

Custom States

StateDescription
:state(unlocked)The trophy has been unlocked

Shadow DOM Parts

PartDescription
iconThe icon container
nameThe trophy name label
tooltipThe description tooltip

A declarative achievement tile. Place <game-trophy> elements anywhere in the game layout — typically grouped together on the result screen or in a persistent HUD area. Each trophy manages its own locked/unlocked visual state and integrates with the shell's trophy tracking.

Icons are rendered using <game-icon>, which reads the sprite sheet URL from the shell's spriteSheet signal (set via the sprite-sheet attribute).

Attributes

id
string -- Unique trophy identifier. Used for persistence and for condition lookups via when-some-trophy="id".
name
string -- Display name shown below the icon.
icon
string -- Icon name. Renders a <game-icon name="..."> element, which reads the sprite sheet URL from shell.spriteSheet.
description
string -- Tooltip text shown when the trophy is tapped.
when-*
Any condition attribute for auto-unlock. The trophy is automatically unlocked when all conditions pass on entering the result state.
<game-trophy
  id="scorer"
  name="Scorer"
  icon="star"
  when-min-score="10"
  description="Score 10 or more"
>
</game-trophy>

<game-trophy
  id="hat-trick"
  name="Hat Trick"
  icon="fire"
  when-min-streak="3"
  description="Get 3 correct in a row"
>
</game-trophy>
<!-- Note: "streak" resolves via the stats map. It must be kept up to date
     via GameStatUpdateEvent("streak", n). <game-quiz> does this automatically.
     For custom games, dispatch the event yourself. -->

<game-trophy
  id="collector"
  name="Collector"
  icon="chest"
  when-min-trophy-count="5"
  description="Unlock 5 other trophies"
>
</game-trophy>

Properties

.unlocked
boolean -- true if the trophy has been unlocked.
.trophyId
string -- The element's id attribute.

Methods

.unlock()
Unlock this trophy. Idempotent — calling it on an already-unlocked trophy is a no-op. Registers the unlock with the shell (updating shell.trophyCount and shell.isTrophyUnlocked()), persists to localStorage (and to the remote trophy server when trophy-url is configured), and dispatches a game-trophy-unlock event.

Events Dispatched

game-trophy-unlock
Dispatched when the trophy is unlocked. Bubbles to the shell. Carries .trophyId and .trophyName.
shell.addEventListener("game-trophy-unlock", (e) => {
  console.log(`Unlocked: ${e.trophyName} (${e.trophyId})`);
});

Auto-Unlock

Trophies with condition attributes are checked automatically when the game enters the result state. Any trophy whose conditions all pass is unlocked via .unlock().

Tooltip

Clicking or tapping a trophy shows its description in a tooltip for 1.8 seconds.

Persistence

Unlocked trophy IDs are stored as a JSON array in localStorage under the key {storage-key}-trophies. On connect, if the shell has the trophy registered as already unlocked, the visual state is restored immediately.

Remote sync

When the shell has a trophy-url attribute, trophies are also persisted to a remote server so they survive across devices and browsers.

<game-shell
  game-id="my-game"
  trophy-url="https://trophies.htmlgamekit.dev"
>
  <game-trophy id="first-win" name="First Win" when-min-score="1"></game-trophy>
</game-shell>

On load, the shell fetches the player's remote trophies and merges them with localStorage (union of both sets). Any trophy present on the server but missing locally is unlocked in the UI.

On unlock, the trophy is persisted to both localStorage and the remote API immediately. No batching or manual save step is needed.

Player identity is an anonymous UUID generated by the server on first contact and stored in localStorage under {gameId}-player-id. Each game gets its own independent player identity. The UUID is sent on subsequent requests as an Authorization: Bearer {uuid} header. There is no signup or login — identity is automatic.

Graceful degradation — if the server is unreachable, localStorage remains the source of truth and the game works normally. Remote sync resumes on the next successful request.

Standalone usage

The trophy service can also be used directly without <game-shell>:

import { gameTrophies } from "htmlgamekit/trophies";

const trophies = gameTrophies("my-game", {
  baseUrl: "https://trophies.htmlgamekit.dev",
});

// Fetch all unlocked trophies (returns {id, unlocked_at} objects)
const unlocked = await trophies.fetchTrophies();

// Unlock a trophy (idempotent PUT, no request body)
await trophies.unlockTrophy("first-win");

// Read the player UUID
console.log(trophies.playerId);

Signal Access

Signal Usage
shell.scene Watches for "result" to trigger auto-unlock checks
shell.score, shell.round, shell.stats, shell.difficulty, etc. Read by matchesConditions() for when-* auto-unlock conditions

CSS Custom Properties

Property Default Description
--game-trophy-color #fbbf24 Icon color for unlocked trophies

Usage

<div when-some-scene="result" data-overlay>
  <game-result-stat label="Score"></game-result-stat>

  <div style="display: flex; gap: 8px; justify-content: center;">
    <game-trophy
      id="scorer"
      name="Scorer"
      icon="star"
      when-min-score="10"
      description="Score 10 or more"
    >
    </game-trophy>

    <game-trophy
      id="hat-trick"
      name="Hat Trick"
      icon="fire"
      when-min-streak="3"
      description="Get 3 correct in a row"
    >
    </game-trophy>

    <game-trophy
      id="speed-demon"
      name="Speed Demon"
      icon="bolt"
      description="Complete the game in under 10 seconds"
    >
    </game-trophy>
  </div>

  <button commandfor="game" command="--restart">Play Again</button>
</div>

The speed-demon trophy has no auto-unlock condition, so it must be unlocked programmatically:

shell.addEventListener("game-lifecycle", (e) => {
  if (e.action === "result") {
    const totalTime = e.state.roundScores.reduce((a, b) => a + b, 0);
    if (totalTime < 10000) {
      document.querySelector("game-trophy#speed-demon").unlock();
    }
  }
});