Drop-in adoption
Bring one JGengine system into the game you already have.
You don't have to adopt an engine to use its parts. Pull in a minimap, health, damage, or XP — over your own entities, store, and loop — with a single install and a tiny adapter. Here's how each one drops in, and the honest tradeoffs.
Install one package
bun add @jgengine/core (and @jgengine/react for UI). Zero-dep core.
Write a tiny adapter
A get/set over your store, or one entity → marker projection.
Call it from your loop
Apply results and tick(dt) yourself. Your renderer never changes.
🗺️Minimap
Render a minimap from the array you already have
Project your own units into display markers and hand them to the minimap. It reads a static array, a subscribable store, or a native MarkerSet — no copying entities into JGengine.
- You keep
- Your entity array/ECS/store, your styling and chrome.
- You get
- A framed minimap with categorized markers and a facing arrow — no GameProvider, no three.js.
import { Minimap } from "@jgengine/react/map";// units are YOUR objects — nothing is mirrored into a JGengine store.const markers = units.map((u) => ({ id: u.id, position: [u.x, 0, u.z], kind: u.team }));<Minimap markers={markers} center={[cam.x, cam.z]} worldRadius={120} />;❤️Health & resources
Health, shields, mana — over your own state
A serializable current/max/min pool with a two-method adapter. Nothing privileges “health”; the same primitive drives shields, stamina, durability, or any named resource in the store you already own.
- You keep
- Where the numbers live — a plain object, Zustand, Redux, an ECS component, a DB row.
- You get
- Clamped, inspectable pool changes with the exact applied amount and min/max flags.
import { applyStatPoolDelta, createStatPool } from "@jgengine/core/stats/statPool";const pools = { hero: { health: createStatPool({ current: 80, max: 100 }) } };const access = { get: (id, stat) => pools[id]?.[stat] ?? null, set: (id, stat, next) => { pools[id][stat] = next; },};const hit = applyStatPoolDelta(access, "hero", "health", -12); // hit.applied === -12⚔️Damage
Resolve a hit, keep the provenance
A pure, deterministic damage resolver: channel-vs-trait matchup, receiver modifiers, and a status roll against an injected RNG. It returns the final impact and every stage — you apply it to whatever holds your entities' health.
- You keep
- Your entities, your health sink, when a hit happens.
- You get
- A serializable, replay-safe resolution you can run on the authority and mirror to clients.
import { resolveDamageHit } from "@jgengine/core/combat/damageResolution";import { applyStatPoolDelta } from "@jgengine/core/stats/statPool";const result = resolveDamageHit({ channel: "kinetic", impact: 24, target: enemy.id, targetTraits: enemy.armored ? ["armored"] : [], matchup: { entries: { kinetic: { armored: { impact: 0.5 } } } },});// Drain it through the same two-method adapter from health.ts above.applyStatPoolDelta(access, enemy.id, "health", -result.impact); // -12 vs the armored enemy✨XP & leveling
XP curves and level-ups on your save format
Deterministic XP curves, surplus rollover, level caps, and one event per reached level — written back through a two-method adapter over the player state you already persist.
- You keep
- Player identity, your save schema, your event bus, when XP is granted.
- You get
- grantXp: curve math, rollover, caps, and ordered level-up callbacks with no store to adopt.
import { leveling } from "@jgengine/core/game/progression";const track = leveling({ xpForLevel: { kind: "power", base: 80, exponent: 1.35, round: "floor" }, maxLevel: 20 });// access reads/writes save.players[id].xp / .level — your data, your store.const gained = track.grantXp(access, "p1", 900, (level) => emit("level-up", { level }));The tradeoffs
What you get, and what you take on.
Drop-in adoption is a deliberate deal: a tiny surface and total ownership of your world, in exchange for wiring the SDK deliberately doesn't do for you.
Why it's worth it
Add one system, not an architecture
Keep your renderer, entity store, React setup, and game loop. You import a single capability, not a framework that wants to own main().
Your data stays yours
A small get/set (or one projection) adapter reads the state you already own. Nothing is mirrored into a parallel JGengine store to use a calculation or renderer.
Serializable & deterministic
State is plain data; time and randomness are injected. The same pieces drop into saves, multiplayer authority, replays, and tests without special-casing.
Tree-shakable, zero-dep core
Direct imports like @jgengine/core/stats/statPool pull only what you touch. The core package has no dependencies of its own.
What you take on
You write the adapter and the wiring
The get/set, the projection, the raycast, and the tick/update call site are yours. It's a few lines, but the SDK deliberately doesn't reach into your world for you.
Presentation stays your job
These are calculations, renderers, and mechanics — not a turnkey game. Rendering, input, audio, and persistence remain on your side of the seam.
Some systems still want native integration
The full runtime, the authoritative multiplayer host, and editor-authored scenes assume a JGengine host. A portable kernel exists underneath where practical, but the drop-in path stops there.
Explicit beats magic
You call tick(dt) and apply results yourself. That's more wiring than a black box — and the reason it fits engines you didn't write.
How to read a capability
Every system is one of three modes.
Before you adopt one, you know exactly how deep it reaches into your project.
Pure portable
Plain data in, plain data out. No runtime, renderer, store, or React. Curves, matchups, pool math.
Adapter portable
Works over your state through a small structural interface. You keep ownership of entities, updates, and rendering.
Native integration
Uses a JGengine host (GameContext, the shell). Reserved for what genuinely needs it, with a portable kernel underneath.
More drop-in systems land as they're built — inventory, quests, weapon plumbing, and multiplayer plumbing follow the same adapter shape.