npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@mundogamernetwork/mg-city

v0.6.7

Published

Mundo Gamer City 2D engine - framework-free TypeScript + Phaser 3 package powering MGN's interactive onboarding city

Downloads

1,961

Readme

@mundogamernetwork/mg-city

Framework-free TypeScript + Phaser 3 engine for Mundo Gamer City — MGN's interactive 2D onboarding experience. A side-view linear avenue (visual ref: hi-res pixel art, large expressive characters, golden-hour parallax) where MGN platform buildings appear left→right in the order of the selected profile's journey, with sequential gating. The engine renders the city, entities and in-canvas fx only; all real UI (dialogue box, HUD, chapter cards, menus) is the host's DOM overlay, driven through a typed event bus. It never imports Vue/Nuxt/Pinia.

Install

npm install @mundogamernetwork/mg-city phaser

phaser (^3.80) is a peer dependency. Only runtime dep: mitt (event bus). Movement is pure 1D walk left/right + interact — no pathfinding library, no jumping.

Public API

import {
  createGame,
  destroyGame,
  createCityBus,
  parseContentBundle,
  computeCoinTotals,
  type ContentBundle,
  type CityGameHandle,
  type CityBus,
} from '@mundogamernetwork/mg-city'

const bus = createCityBus()
const handle: CityGameHandle = createGame(containerEl, bundleJson, {
  bus,
  profileSlug: 'indie-developer',      // orders the avenue + colors the avatar
  completedSlugs: ['community'],       // host/server-owned progression
  collectedCoinIds: ['community_c1'],  // host/server-owned coin state
  assets: [{ key: 'sign_jobs', url: '/signs/sign_jobs.png' }], // official pixel plates
})

handle.journey        // resolved left→right avenue order
handle.getCoinTotals() // { global, perDistrict } collectible totals

// … later, on route leave:
destroyGame(handle) // or handle.destroy()

createGame(container, contentBundle, options?):

  • Validates the bundle with parseContentBundle (throws BundleParseError with a precise message; pass skipValidation: true if already validated).
  • Boots Phaser with Scale.RESIZE (fills the container), AUTO renderer (WebGL with Canvas fallback; force with renderer: 'canvas'), pixelArt: true and pixel-safe camera zoom snapping (never fractional), DPR capped at 2 (maxDpr).

Ownership model (important)

  • Progression is HOST-owned. The engine renders gating from completedSlugs / ui:set-progress; the host decides what "completed" means (exit interior, close chapter card, finish mission…). Completed buildings stay re-enterable — backtracking for missed coins is a feature.
  • Coin state is HOST-owned. city:coin-picked is an intent; the host/server validate and persist. On (re)boot, collectedCoinIds seeds what never respawns; missed coins respawn on revisit.
  • Dialogue actions are HOST-resolved. The engine emits city:dialogue-open and locks input; the host walks the node tree, executes actions (open_link, bookmark_feature, conversion_cta, …) and replies ui:dialogue-closed.
  • Presence networking is HOST-side. The host joins its presence channel, publishes the throttled city:player-moved, and feeds peers via ui:presence-update. The engine only renders ghosts.

Event bus contract

The host listens to city:* and sends ui:*. All payloads are typed (src/types/events.ts).

| Engine → host | Payload | When | |---|---|---| | city:ready | {version, locale, journey, coinTotals} | Avenue created — echoes the resolved order + collectible totals | | city:load-progress | {progress: 0..1} | Asset preload | | city:building-approach | {slug, locked} | Player nears a building door (locked = sequential gating) | | city:interactable | {kind: 'npc'\|'door'\|'prop'\|'stairs'\|'exit_door'\|null, targetId, districtSlug} | Player entered/left an interaction zone (null = left). Stairs report targetId: 'stairs_up'\|'stairs_down' | | city:district-enter / city:district-exit | {districtSlug, interior, via?} | Interior entered / exited. via:'door' (ground-floor entry door → back AT the building) or via:'exit' (far-end exit door on the final floor → re-placed just PAST the building; forward flow) | | city:chapter-open | {districtSlug, chapter} | Door of a district without interior — host shows the chapter card | | city:dialogue-open | {dialogueId, npcId, districtSlug} | NPC interaction. Engine locks player input until ui:dialogue-closed | | city:coin-picked | {spawnKey, value, districtSlug, x, y} | Player touched a coin (host owns the real balance / server sync) | | city:prop-interact | {propId, kind, districtSlug} | Player used an interactive prop (feed composer, key kiosk, vacancy board…). Engine locks player input until ui:dialogue-closed — the host opens a simulation modal | | city:mission-step | {missionId, districtSlug, step, payload?} | Mission progress intent (server validates) | | city:easteregg | {eggId, districtSlug} | Easter-egg trigger hit | | city:player-moved | {x, districtSlug, moving} | Own avenue position, throttled ~1/s (feed your presence channel) | | city:walk-arrived | {x} | A ui:walk-to command finished (guided tour chaining) |

| Host → engine | Payload | Effect | |---|---|---| | ui:dialogue-closed | {dialogueId, outcome?} | Unlocks player input | | ui:set-progress | {completedSlugs} | Host-owned progression → re-render gating (✓ / next / locked) | | ui:walk-to | {x} | Guided tour: auto-walk the player to x with the normal walk anim | | ui:teleport | {districtSlug \| null} | Place player at a district door (or the avenue start) | | ui:move | {dir: 'left'\|'right'\|null} | Host-DOM mobile arrows (null = release) | | ui:presence-update | {players: [{id, x, spriteVariant, label?}]} | Full peer snapshot ~1/s; engine diffs by id, interpolates, caps 15 ghosts | | ui:audio | {music?, sfx?} (boolean or {enabled, volume?}) | Starts/stops the music loop, gates SFX. Both channels muted by default — send the first enable after a user gesture (autoplay policy; engine also guards with Phaser's locked-audio handling) | | ui:sfx | {key} | Host passthrough to play an engine-loaded SFX for Vue-side moments (dialogue node advance, chapter/simulation complete) | | ui:input-lock | {locked} | Hard input lock (e.g. while a modal is open) | | ui:pause / ui:resume | — | Pause/resume the active scene |

ContentBundle contract (v0.2 — avenue model)

Full types in src/types/bundle.ts; committed example in fixtures/mock-bundle.json (10 districts, 3 full interiors, 16 profiles). There are no plots — avenue ordering is fully journey-driven. Shape summary:

{
  "meta": { "version": "…", "locale": "en" },
  "districts": [{
    "slug": "jobs",                       // unique — duplicates rejected at parse
    "exterior": { "spriteKey": "…", "sign": { "text", "color", "iconUrl?", "spriteKey?" } },
    "interior": {                          // or null → door emits city:chapter-open
      "template": "office|studio|hall|plaza",   // wide multi-floor cutaways (plaza = 2 floors)
      "theme": { "accent": "#2E86DE", "secondary?": "…", "floor?": "…", "wall?": "…" },
      "npcs": [{ "id", "spriteKey", "slot?": "npc_slot_1..4", "x?", "floor?": "ground|floor_2", "vignette", "dialogueId", "name?", "facing?": "left|right" }],
      "props": [{ "id?", "slot?": "screen_slot_1..2", "x?", "media": { "kind": "image|video|embed", "url" },
                  "featureId?", "floor?", "interactive?": { "kind": "feed_composer|key_kiosk|…" } }],  // slot=wall screen, x=floor kiosk
      "coins": [ { "key", "x", "y", "value", "floor?" },             // point spawn (room-space coords)
                 { "key", "x", "y", "width", "height", "count", "value" } ], // area spawn → keys `${key}#${i}`
      "missions": [{ "id", "type", "objective", "coinReward", "title?" }],
      "easterEggs": [{ "id", "trigger": { "kind": "x", "x", "radius?" }, "coinReward", "hint?" }]
    },
    "chapter": { "title", "ctaUrl", "recommendTags": [] }
  }],
  "profiles": [{ "slug", "label", "spriteVariant": { "body", "outfit", "accessory", "palette" },
                 "recommendedDistricts": [] }],   // ordered journey — these buildings come FIRST
  "dialogues": { "dlg-id": { "id", "start", "nodes": { "n1": { "id", "type": "line|choice|action", "speaker?", "text?", "next?", "choices?", "action?" } } } }
}

Notes for backend/frontend teams:

  • Avenue order = profile's recommendedDistricts first (in that order), then every other district in bundle order. computeJourneyOrder is exported if the host needs it.
  • Sequential gating: only the first uncompleted building in the journey is enterable; earlier ones show ✓ and remain enterable; later ones are locked. State comes from the host, never the engine.
  • Coin spawn keys are the idempotency contract with POST /city/sessions/{uuid}/coins. Area spawns expand to ${key}#${index}. computeCoinTotals gives the HUD's "collected/total" denominators per district and globally.
  • Validation fails loudly at boot: duplicate district slugs, profiles recommending unknown districts, NPCs referencing missing dialogues, dialogues with missing start nodes, malformed signs.
  • Vignettes (idle, idle_typing, buried_in_papers, chasing, recording, studying, interviewing, browsing_feed, posting, presenting, queueing, celebrating) are code-authored in the engine; bundles select them by id. NpcDef.facing flips the sprite so paired vignettes (two interviewing NPCs) face each other.
  • Interactive props (interactive: {kind}) get the same prompt flow as NPCs; confirming emits city:prop-interact and locks input until the host replies ui:dialogue-closed. propId = PropDef.id, falling back to the slot name or x:<px>.
  • Interiors are wide multi-floor cutaways (v2): templates define stacked floors (2-3 screens wide each; camera scrolls horizontally and eases vertically). NPCs/props/coins address a floor by id (floor: 'floor_2', default ground); slot names resolve within that floor; coin coords stay room-space so totals math is unchanged. Floors connect through x-aligned stair zones (walk in + interact; city:interactable {kind:'stairs', targetId:'stairs_up'|'stairs_down'}). The final floor's far-end exit door (kind:'exit_door') emits city:district-exit {via:'exit'} and the avenue re-places the player just past the building — enter left, traverse, exit right. Parse rejects unknown floor ids (TEMPLATE_FLOOR_IDS). Single-floor templates keep working (floors array with one entry; ground-floor mirrors on the template root).
  • Signs never render HD logos in-canvas (breaks pixel art). Rendering order: sign.spriteKey (official pixel-art brand plate PNG, nearest-neighbor at 2×, brand-color glow) → sign.iconUrl (small PNG downscaled to ~18px on the text plate — only for logos that survive it) → text-only pixel plate with the brand color as accent. HD logos live in the host's DOM overlay.

Art & assets

All placeholder art is generated programmatically at preload (Phaser Graphics → generateTexture, once, never per-frame): golden-hour parallax sky/clouds/skylines, textured street with lamp glow/benches/trees, varied facades with lit window grids and brand awnings, articulated ~92px characters (head/torso/legs, 2-frame walk cycle, facing flip, idle bob), coin glow, ambient fireflies. See src/art/.

The official pixel-art brand sign plates live in assets/signs/ (sign_<slug>.png, generated by tools/generate_brand_signs.py) and are loaded by the host via the assets option (the playground uses import.meta.glob). Real hi-res pixel-art atlases (facades, characters) drop in the same way — any bundle spriteKey resolved by a loaded texture skips its procedural placeholder.

Produced pixel art (assets/sprites, assets/backgrounds)

The in-house art set (generated by tools/art/build_all.py; contact sheets in assets/preview/) replaces the placeholders when loaded through the same assets option:

  • char_<profile>.png — 16 sheets, 6 frames of 24x48 (walk0..3 contact/pass cycle @8fps, idle, talk; feet baseline y=45). Selected by profileSlug; presence ghosts pick deterministically among loaded sheets.
  • npc_generic_1..8.png — 4 frames of 24x48 (2-frame idle + walk pair); NPCs without an exact spriteKey texture pick one deterministically per NPC id.
  • facade_<slug>.png + facade_wall-of-legends.png — varied silhouettes (bottom row = sidewalk); the brand sign_<slug> plate anchors in the facade's 76x32 plate zone; the monument closes the avenue after the last building.
  • sky/clouds/skyline_{far,mid,near}.png — tileable parallax strips (distinct scroll rates).
  • interior_<template>.png — 1200x180 floor strips (floor top y=148), tiled per floor; decorative prop_* sprites dress each floor deterministically; prop_kiosk/prop_bigscreen skin the interactive props.
  • coin.png (4-frame spin) + sparkle.png + item_*.png.

Everything renders at ART_SCALE (2x, nearest-neighbor). Every consumer keeps its procedural fallback — a missing key (e.g. an admin-added district without art) degrades gracefully to the placeholder pass. Spritesheet asset defs carry {type:'spritesheet', frameWidth, frameHeight} (see AssetDef in src/assets.ts).

Audio

Chiptune assets live in assets/audio/ (generated by tools/generate_audio.py; regen with python3 tools/generate_audio.py, then python3 tools/convert_audio.py to derive the compressed formats via ffmpeg): music_city_loop (20s seamless 8-bar loop) + SFX (sfx_coin, sfx_door, sfx_dialogue, sfx_mission, sfx_step, sfx_secret, sfx_denied). Each key ships three files — .wav (source), .ogg (Vorbis, ~8x smaller than wav, used by Chrome/Firefox/Android), and .m4a (AAC-LC, used by Safari/iOS — Safari cannot reliably decode ogg/vorbis, hence this fallback). The host registers multi-format audio through the assets option using type: 'audio' with either sources: { ogg, m4a } or urls: [...] (instead of a single url); AssetDef/manifestEntryFor in src/assets.ts turn that into the ordered URL list Phaser's load.audio(key, urls) uses, and the sound manager auto-picks whichever format canPlayType reports as supported. A bare single url (or a def with no sources/urls) still works — it's classified as audio by extension, same as before. If ffmpeg isn't on PATH, tools/convert_audio.py prints the exact commands to run later instead of silently skipping.

The AudioSystem (src/audio.ts) keeps both channels muted/stopped by default; the host enables them with ui:audio (first enable after a user gesture). Engine-side SFX: coin pickup, door enter/exit, dialogue/prop-interact open, easter egg (sfx_secret), locked-door attempt (sfx_denied), and footsteps at ~0.28s cadence (subtle volume) while walking. Host-side moments (dialogue node advance, chapter/simulation completion) go through ui:sfx {key}. Default volumes: music 0.5, sfx 0.8 — override via the object form of ui:audio.

Playground

npm install
npm run dev    # http://localhost:5199

Full side-view avenue from the mock bundle: pick a profile (reorders the buildings), walk with ←→/AD or tap/click, stop at a door (or press E/SPACE/ENTER) to enter. The sidebar is a minimal HOST implementation: HTML dialogue box + chapter card, MGC collected/total HUD, ui:set-progress loop (exiting an interior / closing a chapter card completes it), mobile arrow zones, a presence-peer simulator, a guided-tour "walk to next building" button, and a live bus-event log.

Scripts

| Command | What | |---|---| | npm run dev | Vite playground | | npm run build | Library build → dist/ (ES + CJS + .d.ts) | | npm run test | Vitest: bundle contract, journey/gating, avenue layout, coins, presence, bus | | npm run typecheck | tsc --noEmit (strict) |