Platform

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:

  1. Sets a boolean flag on the blackboard at key system.achievement.<id>.completed.
  2. Calls gateway.unlock(id) to persist the unlock.
  3. 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).
  4. Records an event to globalMetrics under the Progression category.

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:

FieldTypeNotes
titlestring
descriptionstring
imageObservedValue<string>URL; observable so it can update after creation
classListstring[]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:

ValueString
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

ClassNotes
NullMetricsGatewayDefault; discards all events. Singleton at .INSTANCE.
GoogleAnalyticsMetricsWraps GA4 gtag. Maps categoryevent_category, labelevent_label.
ProxyMetricsGatewayForwards 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());