GameTrophy
Methods
- .effectCallback({ scene })
- .unlock()
- .define(tag, registry)
- .subscribe(context, callback)
Custom States
| State | Description |
|---|---|
:state(unlocked) | The trophy has been unlocked |
Shadow DOM Parts
| Part | Description |
|---|---|
icon | The icon container |
name | The trophy name label |
tooltip | The 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 viawhen-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 fromshell.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
resultstate.<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--trueif the trophy has been unlocked. - .trophyId
-
string-- The element'sidattribute.
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.trophyCountandshell.isTrophyUnlocked()), persists to localStorage (and to the remote trophy server whentrophy-urlis configured), and dispatches agame-trophy-unlockevent.
Events Dispatched
- game-trophy-unlock
-
Dispatched when the trophy is unlocked. Bubbles to the shell. Carries
.trophyIdand.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();
}
}
});