Achievements#
Every game on play.unikia.es can award achievements. Achievements live in the
player's browser: a game needs no account and no server for them. The Hub reads them to
show a Steam-like overview across all games.
This page is the contract. The SDK, @unikia/upe-sdk, is the reference implementation; a
game may copy it, port it, or write its own as long as it follows the rules below.
Why this works without a backend#
All games are served from one origin (https://play.unikia.es/<path>/), so they share
one localStorage. A game writes its unlocks there and the Hub page at / reads
them. Running a game on its own (for example vite dev on localhost:5173) still
works; the unlocks just stay on that origin.
Game id#
Each game has a stable game id: id in its unihub.manifest.json, for
example heated-heist. It never changes once published, even if the game is renamed or
moves to another path.
| Game | Game id | Path |
|---|---|---|
| Torture Chamber | torture-chamber |
/torture-game/ |
| Embers of Hollowmaw | embers-of-hollowmaw |
/rpg-game/ |
| Level Dice | level-dice |
/dice-game/ |
| Signal Fire | signal-fire |
/metroidvania-game/ |
| Heated Heist | heated-heist |
/heist-game/ |
| Re-Elect Me! | re-elect-me |
/city-builder-game/ |
| Demonic Maze | demonic-maze |
/maze-game/ |
| Orbion | orbion |
/orbion/ |
Definitions: achievements.json#
Each game ships achievements.json at the root of its build, next to its
index.html (in a Vite project: public/achievements.json), and names it as
achievements in the manifest. The Hub embeds the definitions in its catalogue when the
game is published, so it can list locked achievements of games the player has never opened.
The JSON Schema achievements.v1.json
holds these rules.
{
"v": 1,
"game": "heated-heist",
"achievements": [
{
"id": "first-score",
"title": "First Score",
"description": "Escape with the loot from any house.",
"icon": "💰",
"points": 10
},
{
"id": "career-criminal",
"title": "Career Criminal",
"description": "Steal $500,000 in total.",
"icon": "🏦",
"points": 25,
"goal": 500000,
"format": "money"
}
]
}
| Field | Required | Meaning |
|---|---|---|
v |
yes | Spec version, 1. |
game |
yes | The game id. |
achievements[].id |
yes | Stable id, a-z, 0-9 and -, unique within the game. Never reuse or rename a published id: players' unlocks are keyed by it. |
title |
yes | Short name, at most 40 characters. |
description |
yes | What to do, one sentence. |
icon |
no | One emoji. The Hub falls back to a trophy. |
points |
no | 5, 10, 25 or 50. Defaults to 10. Roughly: 5 = happens naturally, 10 = a normal goal, 25 = takes skill or time, 50 = mastery. |
hidden |
no | true hides the title and description as "???" until unlocked. Use it for spoilers and jokes, not for normal goals. |
goal |
no | Makes it a progress achievement that unlocks when the progress reaches this number. |
format |
no | How to print progress: "count" (default), "money" ($12,500) or "time" (seconds as m:ss). Only with a goal. |
Order in the file is the display order. Removing an achievement is allowed (old unlocks are ignored); adding new ones at any time is expected.
With the SDK:
import { Uniplay } from '@unikia/upe-sdk';
const uniplay = new Uniplay({ game: 'heated-heist' });
uniplay.achievements.unlock('first-score');
uniplay.achievements.addProgress('career-criminal', 12500);
uniplay.achievements.setProgress('career-criminal', 182000);
Platinum#
A player who unlocks every achievement of a game earns its platinum: the game's badge on their
profile, in colour instead of greyed out, and 100 more points in every points total the Hub
shows. Give the game a badge with "badge" in its manifest; without one the Hub
cuts a round badge from the cover. Adding achievements later takes platinum away again until
they're unlocked too.
Unlocks: localStorage#
Key: uniplay:achievements:<game id>. Value: JSON.
{
"v": 1,
"unlocked": { "first-score": 1790000000000 },
"progress": { "career-criminal": 182000 }
}
unlockedmaps an achievement id to the unlock time (milliseconds since epoch).progressmaps a progress achievement id to its current value. Progress only moves up; a write never lowers it.- Writers must re-read the key right before writing and merge (union of
unlocked, keeping the earliest timestamp; maximum of eachprogress), because the same game may be open in two tabs. - Readers must ignore unknown ids, unknown fields and malformed values, and treat a missing or unparsable key as "nothing unlocked".
- Storage can be unavailable (private mode, blocked storage). Games must keep working and simply not record achievements.
Each game writes only its own key. The Hub page only reads, and copies unlocks to the player's account and back when they're signed in, merging them the same way.
Unlock notification#
When an achievement unlocks, the game shows a short, non-blocking notification ("Achievement unlocked" + icon + title) for about 5 seconds. The SDK draws one in the top-right corner on top of the game, or in the Hub's alert corner when the game runs inside the Hub. A game may draw its own instead, in its own style, as long as it shows the title.
Live updates#
The Hub listens to the storage event, so unlocks made in another tab show up
without a reload. Games do not need to do anything for this.
