Game services
Achievements, localization, notification queue, and analytics — the platform services Meep ships for tracking progress, translating text, surfacing messages, and recording events.
Meep includes four platform-level services that most games need: an achievement system that persists unlocks and presents in-game notifications, a localization layer for multi-language string lookup with template interpolation, a notification queue for surfacing messages to the player, and a metrics system for recording categorized events to pluggable analytics backends.
Achievements
AchievementManager is an EnginePlugin. Acquire it via the plugin system,
load definitions from JSON, attach a blackboard for condition tracking, then
initialize the persistence gateway.
import { AchievementManager }
from "@woosh/meep-engine/src/engine/achievements/AchievementManager.js";
const ref = await engine.plugins.acquire(AchievementManager);
const achievements = ref.getValue();
// load achievement definitions from data/database/achievements/data.json
await achievements.loadDefinitions(engine.assetManager);
// attach a blackboard so conditions can be evaluated
achievements.attachBlackboard(myBlackboard);
// sync with the persistence gateway
await achievements.initializeGateway();
Achievement definitions
Each entry in the definitions JSON describes one achievement:
[
{
"id": "first_kill",
"condition": "combat.kills >= 1",
"icon": "data/textures/achievements/skull.png",
"secret": false
}
]
The condition string is a BlackboardTrigger expression evaluated against
the attached blackboard. When the expression becomes true, the achievement
unlocks automatically. Setting secret: true hides the achievement in the UI
until it unlocks.
Unlocking and presenting
achievements.unlock(id) unlocks an achievement by ID directly — useful for
code-driven milestones that do not map to a blackboard condition:
achievements.unlock("first_kill");
On unlock the manager:
- Sets a boolean flag on the blackboard at key
system.achievement.<id>.completed. - Calls
gateway.unlock(id)to persist the unlock. - Spawns a temporary GUI entity that plays an animated notification card and a
sound effect (
data/sounds/effects/Magic_Game_Essentials/Magic_Airy_Alert.wav). - Records an event to
globalMetricsunder theProgressioncategory.
Localization keys achievement.<id>.title and achievement.<id>.description
are used for the card text.
Persistence gateway
An AchievementGateway is the interface between the manager and whatever
backend stores unlocked IDs. The manager gets its gateway from
engine.platform.getAchievementGateway(). The bundled
StorageAchievementGateway backs it with the engine’s Storage API:
import { StorageAchievementGateway }
from "@woosh/meep-engine/src/engine/achievements/gateway/StorageAchievementGateway.js";
class MyPlatform extends WebEnginePlatform {
getAchievementGateway() {
return new StorageAchievementGateway(this.getStorage(), "game.achievements");
}
}
To connect to a platform-native achievement API (Steam, Xbox, etc.), extend
AchievementGateway and implement getUnlocked() (returns Promise<string[]>)
and unlock(id) (returns Promise).
Localization
engine.localization is a Localization instance. It is wired to the asset
manager at engine construction and is ready before startup resolves.
Loading a locale
Localization data lives at data/database/text/<locale>.json by default. Call
loadLocale with a BCP-47 locale code:
await engine.localization.loadLocale("en-gb");
// or from a URL hash: engine.localization.loadLocale("fr-fr");
The loader also fetches data/database/text/languages.json, which maps locale
codes to LanguageMetadata (including reading_speed in characters per second
and fallback language chains). After a successful load, engine.localization.locale
(an ObservedString) updates to the new locale code and fires its onChanged
signal so UI can re-render.
Retrieving strings
// simple key lookup
const label = engine.localization.getString("ui.start_button");
// template with variable seed
const msg = engine.localization.getString("hud.score", { value: 1200 });
// template source: "Score: {value}" → "Score: 1200"
If a key is missing in debug mode, getString logs the closest matching keys
by string similarity and returns @<key> as a placeholder.
hasString(key) checks for key existence without logging a warning.
Formatters
// locale-aware integer formatting (groups of thousands)
engine.localization.formatIntegerByThousands(1000000);
// → "1,000,000" in en-gb
// estimated reading time in seconds
engine.localization.estimateReadingTime("Hello world");
Validation
In a build pipeline or test, localization.validate(errorConsumer) checks
every template string for parse errors and calls errorConsumer(key, error, original) for any that fail:
localization.validate((key, err) => {
console.error(`Bad template at '${key}':`, err);
});
Notification queue
NotificationLog is a simple observable queue for surfacing messages to the
player. It is not directly exposed on engine — you create an instance and
display it via your own UI.
import { NotificationLog }
from "@woosh/meep-engine/src/engine/notify/NotificationLog.js";
const log = new NotificationLog();
log.maxLength = 100; // default: 1000
const n = log.add({
title: "Item found",
description: "You picked up a health pack.",
image: "data/textures/items/health.png",
classList: ["pickup"]
});
log.elements is an observable List<Notification>. Subscribe to it to update
your UI:
log.elements.on.added.add((notification) => {
renderToastCard(notification);
});
Each Notification carries:
| Field | Type | Notes |
|---|---|---|
title | string | |
description | string | |
image | ObservedValue<string> | URL; observable so it can update after creation |
classList | string[] | CSS class names for your notification component |
When elements.length reaches maxLength, the oldest entries are dropped to
make room for new ones.
Metrics
globalMetrics is a process-wide ProxyMetricsGateway (exported from
engine/metrics/GlobalMetrics.js). By default it routes to a
NullMetricsGateway (all events are silently dropped). Swap in a real backend
by calling setTarget:
import { globalMetrics }
from "@woosh/meep-engine/src/engine/metrics/GlobalMetrics.js";
import { GoogleAnalyticsMetrics }
from "@woosh/meep-engine/src/engine/metrics/GoogleAnalyticsMetrics.js";
globalMetrics.setTarget(new GoogleAnalyticsMetrics("G-XXXXXXXX"));
Recording events
import { MetricsCategory }
from "@woosh/meep-engine/src/engine/metrics/MetricsCategory.js";
globalMetrics.record("level_complete", {
category: MetricsCategory.Progression,
label: "level_03"
});
record(type, event) passes both arguments through to the active gateway.
type is a free-form event name string; event is an arbitrary object.
Categories
MetricsCategory provides conventional category strings:
| Value | String |
|---|---|
MetricsCategory.Interaction | "interaction" |
MetricsCategory.Economy | "economy" |
MetricsCategory.Combat | "combat" |
MetricsCategory.Unit | "unit" |
MetricsCategory.Progression | "progression" |
MetricsCategory.System | "system" |
The engine itself records achievement unlocks under Progression.
Backends
| Class | Notes |
|---|---|
NullMetricsGateway | Default; discards all events. Singleton at .INSTANCE. |
GoogleAnalyticsMetrics | Wraps GA4 gtag. Maps category → event_category, label → event_label. |
ProxyMetricsGateway | Forwards to a swappable target gateway. Used by globalMetrics. |
Implement a custom backend by extending MetricsGateway and overriding
record(type, event):
import { MetricsGateway }
from "@woosh/meep-engine/src/engine/metrics/MetricsGateway.js";
class MyAnalytics extends MetricsGateway {
record(type, event) {
myService.send({ event: type, ...event });
}
}
globalMetrics.setTarget(new MyAnalytics());