@walangstudio/badgetrip-vue

Vue 3 plugin, composables and an AchievementBadge for badgetrip.
353
Install
npm install @walangstudio/badgetrip-vue
Latest Version:0.1.0
Vue:3
License:MIT
Last Updated:Oct 21, 2026
Links: GitHub  ·  npm
Maintainer: scr1p7k177y

badgetrip

release license typescript CI core deps

Points, streaks, tiers, leaderboards and achievements for any JavaScript app. You emit events for what your users did. The engine calculates their score, their streaks, and which achievements they just unlocked. Each achievement is shown as a badge.

const result = await engine.emit({ id, actor: 'ana', type: 'habit.done', ts: Date.now(), payload: {} });
result.unlocked; // ['first_step']

Status: early (0.x). APIs may change before 1.0. Changes are listed in the CHANGELOG.

Why

Every app with a streak or a badge rebuilds the same things: a points counter, a handful of unlock rules, a streak that resets, a leaderboard. The logic is often split across database triggers, API handlers and UI hooks.

badgetrip keeps all the rules in one place, and leaves the rest of your app to you:

  • Your database. The engine talks to four small store interfaces. Implement them on Postgres, SQLite, KV or anything else, and badgetrip keeps its state next to your tables.
  • Your framework. The core is plain TypeScript with no dependencies. Small adapter packages cover React, React Native, Vue, Angular, plain HTML, htmx, Electron and Tauri.
  • Your clock. The engine never reads the system time on its own. Same events in, same state out, so replays and tests are deterministic.

Features

  • Achievements as config. A keyed object with rule builders: rules.count('habit.done', 10), rules.streak('daily', 7), rules.all(...). Tiers expand one entry into bronze/silver/gold. Hidden achievements stay concealed until earned.
  • Built-in progress. engine.catalog(actor) returns every achievement, ready to display, with { current, target, percent }. The actor is the user an event belongs to.
  • Points, streaks, tiers, leaderboards, escalators. All-time and rolling leaderboards, per-key streaks, and capped severity escalators with decay.
  • Idempotent. Events carry an id. A retry or a double click counts once.
  • Validated up front. createEngine rejects typos and impossible rules before the first event.
  • Swappable icons. 18 built-in SVG icons, including an animated one. Override any icon per achievement, per tier series or per category, with GIFs, PNGs or your own SVGs.
  • Accessible badges. Every adapter shows a still frame while locked or when the user prefers reduced motion, and labels its progress bar.
  • Unlock celebrations. Toasts (small popup cards) in any corner or edge, a modal, or fullscreen with confetti, with optional sound. Configure once, override per achievement. A hidden achievement shows as "Hidden achievement" until it's unlocked; secret mode goes further and leaves hidden achievements out of the list, showing only how many remain.
  • Themes. One object sets colors, icons and celebrations. Swap it at runtime, extend a built-in (classic, dark, arcade, minimal, aurora), or share yours as a package. Backgrounds and icons can be gradients.
  • Animations. Popups enter and leave the way you choose (fade, slide from any side, scale, pop or bounce), with their own speed and easing, per theme or per achievement. Badges can be GIFs. Everything stays still for people who prefer reduced motion.
  • Progress you can see. Badges show "3/5" under locked achievements, and optional popups report progress along the way, every step or at milestones.
  • Cross-process. @walangstudio/badgetrip-ipc runs the engine in Electron's main process or a worker, protected by an allowlist of methods and an authorize hook.

Try it

The playground is a sandbox: edit achievements, a theme, animations, sounds and popups in tabs, written just like the examples in these docs, then emit events and watch achievements unlock with the real celebrations. Nothing is saved. To run it locally:

pnpm install
pnpm --filter @badgetrip-example/playground dev

Install

npm install @walangstudio/badgetrip-core
npm install @walangstudio/badgetrip-react   # or vue, angular, react-native, html

Quick start

import {
  createEngine,
  defineAchievements,
  memoryAchievementStore,
  memoryEventStore,
  memoryScoreStore,
  memoryStreakStore,
  rules,
  systemClock,
} from '@walangstudio/badgetrip-core';

const achievements = defineAchievements({
  first_step: {
    name: 'First step',
    description: 'Log your first habit',
    icon: 'sprout',
    when: rules.count('habit.done', 1),
  },
  regular: {
    name: 'Regular ({tier})',
    description: 'Log {n} habits',
    when: rules.count('habit.done'),
    tiers: { bronze: 10, silver: 50, gold: 200 },
  },
});

const engine = createEngine({
  events: memoryEventStore(),
  scores: memoryScoreStore(),
  achievements: memoryAchievementStore(),
  streaks: memoryStreakStore(),
  clock: systemClock,
  definitions: {
    scores: ['xp'],
    points: [{ on: 'habit.done', score: 'xp', delta: 10 }],
    achievements,
  },
});

const result = await engine.emit({
  id: crypto.randomUUID(),
  actor: 'ana',
  type: 'habit.done',
  ts: Date.now(),
  payload: {},
});

result.unlocked;                // ['first_step']
await engine.score('ana', 'xp'); // 10
await engine.catalog('ana');     // every badge with its progress

The memory stores are for demos and tests. For data that survives a restart, see Bring your own store.

Usage

The examples below build on one engine for a habit tracker. The comments show the real output.

Define the rules

const HOUR = 3_600_000;
const DAY = 24 * HOUR;

const engine = createEngine({
  events: memoryEventStore(),
  scores: memoryScoreStore(),
  achievements: memoryAchievementStore(),
  streaks: memoryStreakStore(),
  clock: systemClock,
  definitions: {
    scores: ['xp'],
    points: [{ on: 'habit.done', score: 'xp', delta: 10 }],
    streaks: [
      { code: 'daily', tickEvents: ['day.completed'], resetEvents: ['day.missed'], scoping: 'per-actor' },
    ],
    tiers: [
      {
        code: 'level',
        score: 'xp',
        thresholds: [
          { name: 'novice', at: 0 },
          { name: 'adept', at: 100 },
          { name: 'master', at: 500 },
        ],
      },
    ],
    leaderboards: [
      {
        code: 'weekly',
        source: { kind: 'score', score: 'xp' },
        window: { type: 'rolling', ms: 7 * DAY },
        limit: 10,
      },
    ],
    achievements: defineAchievements({
      first_step: {
        name: 'First step',
        description: 'Log your first habit',
        icon: 'sprout',
        points: 5,
        when: rules.count('habit.done', 1),
      },
      regular: {
        name: 'Regular ({tier})',
        description: 'Log {n} habits',
        icon: 'medal',
        when: rules.count('habit.done'),
        tiers: { bronze: 10, silver: 50, gold: 200 },
      },
      on_a_roll: {
        name: 'On a roll',
        description: 'Keep a 3-day streak',
        icon: 'flame',
        when: rules.streak('daily', 3),
      },
      night_owl: {
        name: 'Night owl',
        description: 'Log a habit between midnight and 4am',
        lockedDescription: 'Some habits happen after dark',
        icon: 'moon',
        hidden: true,
        when: rules.count('habit.done', 1, { todBetween: [0, 4 * HOUR] }),
      },
    }),
  },
});

createEngine checks all of it up front. A typo like rules.streak('dialy', 3) throws here, not on the first event.

Record what happened

const result = await engine.emit({
  id: 'evt_1',
  actor: 'ana',
  type: 'habit.done',
  ts: Date.now(),
  payload: {},
});

result.scoreDeltas; // [{ actor: 'ana', score: 'xp', delta: 10, reason: 'evt_1', ts }]
result.unlocked;    // ['first_step']

The id makes it safe to retry. Emitting evt_1 again returns empty changes and counts nothing.

Streaks

Streaks count ticks, not calendar days. Emit day.completed once a day for each user who was active that day, and day.missed for those who weren't. Usually a nightly job emits these events.

// Ana's third day in a row
await engine.emit({ id: 'day_3', actor: 'ana', type: 'day.completed', ts: Date.now(), payload: {} });
// streakChanges: [{ code: 'daily', actor: 'ana', from: 2, to: 3 }], unlocked: ['on_a_roll']

await engine.streak('ana', 'daily'); // { current: 3, best: 3, lastTick }

Scores, tiers and leaderboards

Here a tier is a score level (novice, adept), not an achievement tier like bronze or gold.

await engine.score('ana', 'xp');      // 10
await engine.tier('ana', 'level');    // { current: 'novice', next: 'adept', remaining: 90, value: 10 }
await engine.leaderboard('weekly');   // after Ben logs two habits: [{ actor: 'ben', value: 20 }, { actor: 'ana', value: 10 }]

Achievements and progress

await engine.achievements('ana');               // [{ code: 'first_step', at }, { code: 'on_a_roll', at }]
await engine.progress('ana', 'regular.bronze'); // { current: 1, target: 10, percent: 10 }

const badges = await engine.catalog('ana');

catalog returns every achievement in definition order, ready to draw: code, name, description, icon, points, unlocked, and progress. regular shows up as three badges, regular.bronze to regular.gold. night_owl comes back as "Hidden achievement" with its lockedDescription until Ana earns it.

Changing the rules later

  • engine.refresh(actor) awards anything the actor already qualifies for. Run it after adding an achievement.
  • engine.replay(log) rebuilds state from a stored event log, in ts order.
  • engine.seed(snapshot) loads existing scores, achievements and streaks when moving from another system.

Celebrate unlocks

<UnlockNotifier actor="ana" sound celebrations={celebrations} />

Each unlock pops up on top of the page. Give an achievement celebration: 'epic' for fullscreen confetti, or 'modal', 'quiet', or your own preset. The celebrations guide covers positions, sounds, secret mode and styling.

Show it in your UI

Wrap the engine once with the package for your stack, and every badge updates after each emit. See Frameworks.

Frameworks

Start with Getting started, then pick your stack:

Stack Package Guide
React @walangstudio/badgetrip-react React
React Native / Expo @walangstudio/badgetrip-react-native React Native
Vue 3 @walangstudio/badgetrip-vue Vue
Angular 17.1+ @walangstudio/badgetrip-angular Angular
Plain HTML, web components @walangstudio/badgetrip-html HTML
htmx, server-rendered @walangstudio/badgetrip-html htmx
Electron @walangstudio/badgetrip-ipc + a UI package Electron
Tauri a UI package, optionally @walangstudio/badgetrip-ipc Tauri

In React:

import { AchievementBadge, BadgetripProvider, useAchievementCatalog } from '@walangstudio/badgetrip-react';

function Trophies({ userId }: { userId: string }) {
  return useAchievementCatalog(userId).map((a) => <AchievementBadge key={a.code} achievement={a} />);
}

<BadgetripProvider engine={engine}>
  <Trophies userId="ana" />
</BadgetripProvider>;

Packages

Package What it is
@walangstudio/badgetrip-core The engine, rule builders, store interfaces and in-memory stores. No dependencies.
@walangstudio/badgetrip-assets 18 SVG icons and createIconResolver for overrides. No dependencies.
@walangstudio/badgetrip-react Hooks, BadgetripProvider, AchievementBadge, IconProvider.
@walangstudio/badgetrip-react-native The React hooks plus a native AchievementBadge on react-native-svg.
@walangstudio/badgetrip-vue Plugin, composables returning { data, error }, AchievementBadge.
@walangstudio/badgetrip-angular provideBadgetrip, a signal-based BadgetripService, a standalone badge component.
@walangstudio/badgetrip-html renderBadge / renderCatalog HTML strings and <badgetrip-catalog> / <badgetrip-badge> custom elements.
@walangstudio/badgetrip-ipc Serve an engine in one process and use it from another, over MessagePort or Electron IPC.
@walangstudio/badgetrip-testing A steppable clock, event helpers, makeTestEngine, and runStoreContract for custom stores.

Bring your own store

badgetrip ships no database adapters. Implement EventStore, ScoreStore, AchievementStore and StreakStore against the database you already use, and pass them to createEngine:

const engine = createEngine({
  events: myEventStore(db),
  scores: myScoreStore(db),
  achievements: myAchievementStore(db),
  streaks: myStreakStore(db),
  clock: systemClock,
  definitions,
});

Then prove it behaves like the reference stores:

import { runStoreContract } from '@walangstudio/badgetrip-testing';

runStoreContract(() => makeFreshStores());

docs/ADAPTERS.md spells out what each method must do, and examples/adapter-postgres is a complete Postgres implementation to copy. Saving data walks through the options.

How it works

Everything flows from events. For each emit, the engine:

  1. validates the event and queues it behind any in-flight event for the same actor,
  2. appends it to the event store, stopping if its id was already seen,
  3. applies point rules, then streak ticks and resets, then escalators,
  4. evaluates the achievements the user doesn't have yet and awards the ones that pass.

It returns what changed: score deltas, streak changes, escalations and newly unlocked codes. engine.replay(log) rebuilds the same state from a stored log. Details are in docs/ARCHITECTURE.md.

Documentation

The docs are published at walangstudio.github.io/badgetrip. The same files live in docs/.

Doc Covers
Guides Getting started, then one guide per framework
Celebrations Unlock popups, sound, confetti, secret mode
Themes Swappable themes: colors, icons and celebrations in one object
Animations Popup entrances and exits, animated badges, confetti, reduced motion
ACHIEVEMENTS.md Achievement config, tiers, hidden badges, progress, icons
RULES.md Every rule kind, filters and the path convention
ARCHITECTURE.md Interfaces, data model, the emit pipeline, determinism
ADAPTERS.md Writing a store for your database
CROSS_PLATFORM.md Where each package runs and how that was verified
CHANGELOG.md Release notes

Examples

Example Shows
examples/playground A browser sandbox: edit achievements, a theme, animations, sounds and popups in tabs, emit events, see the celebrations
examples/node-cli The engine in a plain Node script
examples/react-spa A Vite app on @walangstudio/badgetrip-react
examples/todont-extract A production ruleset (12 badges) moved onto badgetrip
examples/adapter-postgres A Postgres store that passes the contract suite

Development

Requires Node 22 and pnpm 11.

pnpm install
pnpm build          # tsc -b, then ng-packagr for Angular
pnpm test           # vitest, then the Angular suite
pnpm typecheck      # sources, tests and Angular specs
pnpm lint           # biome
pnpm test:coverage  # line and branch coverage
pnpm docs:dev       # the docs site at localhost:5173/badgetrip/

Run the store contract against Postgres with a throwaway database:

BADGETRIP_PG_URL=postgres://... pnpm --filter @badgetrip-example/adapter-postgres test

Releasing

All packages share one version. Bump it in every package.json, add a dated ## [x.y.z] entry to the CHANGELOG, merge to main, then tag:

git tag -a vX.Y.Z -m vX.Y.Z
git push origin vX.Y.Z

The release workflow checks the tag against every package version and the changelog, runs the full gate, publishes to npm with provenance, and creates the GitHub release from the changelog entry. It needs an NPM_TOKEN repository secret.

License

MIT

Related Packages

@vt7/vue2-composables

Vue2 composables

523 1
@shopware/composables

Shopware Frontends composables for Vue

141,216 230
@tb-dev/vue

Vue utilities

12,617
@alis-build/vue-ideate

Vue 3 composables and components to help integrate Alis Ideate into your product...

2,053 0
@oiij/auto-router

Vue Router utilities and composables for Vue 3

1,394 1

Version History

Version Released Vue License
0.1.0 ^3.4.0 MIT
0.0.1 ^3.4.0 MIT