@jgengine/react 0.3.0 → 0.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md ADDED
@@ -0,0 +1,42 @@
1
+ # Changelog
2
+
3
+ All eight `@jgengine/*` packages are versioned in lockstep, so this one file
4
+ covers every release. Format follows [Keep a Changelog](https://keepachangelog.com);
5
+ each release **leads with a Migrate block** — the concrete steps to move a game
6
+ from the previous version onto the new APIs — because the point of a bump is to
7
+ let consumers pick up the better stuff, not just to list what moved.
8
+
9
+ Agents building on the published SDK can also read this programmatically:
10
+ `import { VERSION, CHANGELOG } from "@jgengine/core/meta/changelog"` gives the
11
+ same data as typed values, so an updater can diff its installed version against
12
+ the latest and surface the migration steps.
13
+
14
+ ## Unreleased
15
+
16
+ _Nothing yet._
17
+
18
+ ## 0.5.0
19
+
20
+ An additive release: **every 0.4.0 API is unchanged**, so upgrading is only a
21
+ version bump. New pure primitives across progression, inventory slots, world
22
+ geometry, and React store bindings.
23
+
24
+ ### Migrate
25
+
26
+ - Bump every `@jgengine/*` dependency to `^0.5.0` (the eight packages version in lockstep).
27
+ - No code change is required — 0.5.0 adds surface, it doesn't move or remove any.
28
+ - Optional: replace a game's hand-rolled `progression/curves.ts` with the new `leveling(...)` track. `leveling({ xpForLevel: { kind: "power", base, exponent, round: "floor" }, maxLevel })` returns `xpForLevel`, `resolve`, and `grantXp(ctx.scene.entity.stats, userId, amount, onLevelUp?)` — a drop-in for the old `xpRequiredForLevel` / `resolveLevelProgress` / `grantXp` exports. `ctx.scene.entity.stats` satisfies the primitive's `LevelingStatAccess` structurally, so no adapter is needed.
29
+
30
+ ### Added
31
+
32
+ - `@jgengine/core/game/progression` — genre-agnostic progression primitive. `curve(spec)` / `evalCurve(spec, x)` evaluate declarative scalar curves (`const`, `linear`, `power`, `geometric`, `steps`, `piecewise`, each with optional `round`/`min`/`max`) for speed-by-level, difficulty-by-wave, loot drop-rate ramps, and similar scaling. `leveling(config)` builds the stateful XP→level track (threshold accumulation, multi-level grants, cap handling, `stat.levelUp` emit) on top of an `xpForLevel` curve.
33
+ - `@jgengine/core/inventory/slotModel` — pure slot-grid primitives (`createSlots`, `placeAt`, `removeAt`, `moveSlot`).
34
+ - `@jgengine/core/world/geometry`, `/world/interiors`, `/world/placement` — pure world primitives: grid snapping, footprint AABBs and overlap, interior/exterior spaces, and placement validation.
35
+ - `@jgengine/react/engineStore` — raw-store React bindings (`useEngineState`, `useEngineStore`, `useEngineEvent`).
36
+ - Pure/functional tiers for the `trade`, `unlocks`, `quest`, and `feed` verbs in `@jgengine/core/game`.
37
+
38
+ ## 0.4.0
39
+
40
+ Baseline release: the eight `@jgengine/*` packages (core, ws, sql, react,
41
+ convex, node, shell, assets) as the first tracked version. No migration —
42
+ this is the floor changelog entries are measured against.
@@ -1,6 +1,8 @@
1
- import type { ReactNode } from "react";
1
+ import { type ReactNode } from "react";
2
2
  import type { InventorySlot } from "@jgengine/core/inventory/inventoryModel";
3
3
  import type { ProximityPrompt as ProximityPromptDef } from "@jgengine/core/interaction/proximityPrompt";
4
+ import type { FeedEntry } from "@jgengine/core/game/feed";
5
+ import type { StatLevelUpEvent } from "@jgengine/core/game/events";
4
6
  export declare function SlotGrid({ inventoryId, className, renderSlot, }: {
5
7
  inventoryId: string;
6
8
  className?: string;
@@ -53,3 +55,22 @@ export declare function DialogueBox({ dialogue, onChoice, className, }: {
53
55
  onChoice?: (choice: DialogueChoice) => void;
54
56
  className?: string;
55
57
  }): import("react").JSX.Element;
58
+ export declare function ToastStack({ action, limit, className, renderToast, }: {
59
+ action: string;
60
+ limit?: number;
61
+ className?: string;
62
+ renderToast?: (entry: FeedEntry, index: number) => ReactNode;
63
+ }): import("react").JSX.Element | null;
64
+ export declare function DeathScreen({ statId, open, className, children, }: {
65
+ statId?: string;
66
+ open?: boolean;
67
+ className?: string;
68
+ children?: ReactNode;
69
+ }): import("react").JSX.Element;
70
+ export declare function LevelUpFlash({ stat, durationMs, className, children, renderFlash, }: {
71
+ stat?: string;
72
+ durationMs?: number;
73
+ className?: string;
74
+ children?: ReactNode;
75
+ renderFlash?: (event: StatLevelUpEvent) => ReactNode;
76
+ }): import("react").JSX.Element | null;
@@ -1,5 +1,7 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
- import { useCurrency, useEntityStat, useInventory } from "./hooks.js";
2
+ import { useEffect, useRef, useState } from "react";
3
+ import { useGameContext } from "./provider.js";
4
+ import { useCurrency, useEntityStat, useFeed, useInventory, useLocalPlayerDead } from "./hooks.js";
3
5
  export function SlotGrid({ inventoryId, className, renderSlot, }) {
4
6
  const slots = useInventory(inventoryId);
5
7
  return (_jsx("div", { className: className, "data-inventory": inventoryId, children: slots.map((slot, index) => (_jsx("div", { "data-slot": index, children: renderSlot !== undefined
@@ -41,3 +43,44 @@ export function KeybindRow({ action, keys, className, }) {
41
43
  export function DialogueBox({ dialogue, onChoice, className, }) {
42
44
  return (_jsx("div", { className: className, "data-dialogue": dialogue.id, children: dialogue.lines.map((line, index) => "choices" in line ? (_jsx("div", { "data-choices": true, children: line.choices.map((choice) => (_jsx("button", { type: "button", onClick: () => onChoice?.(choice), children: choice.label }, choice.label))) }, index)) : (_jsxs("p", { children: [_jsx("span", { "data-speaker": true, children: line.speaker }), _jsx("span", { children: line.text })] }, index))) }));
43
45
  }
46
+ function defaultToast(entry) {
47
+ const data = entry.data;
48
+ if (data?.drops !== undefined) {
49
+ return data.drops.map((drop) => `${drop.item ?? drop.currency ?? "item"} ×${drop.count}`).join(" ");
50
+ }
51
+ return typeof entry.data === "string" ? entry.data : JSON.stringify(entry.data);
52
+ }
53
+ export function ToastStack({ action, limit = 4, className, renderToast, }) {
54
+ const entries = useFeed({ action, limit });
55
+ if (entries.length === 0)
56
+ return null;
57
+ const newestFirst = [...entries].reverse();
58
+ return (_jsx("div", { className: className, "data-toast-stack": action, children: newestFirst.map((entry, index) => (_jsx("div", { "data-toast": true, children: renderToast !== undefined ? renderToast(entry, index) : defaultToast(entry) }, `${entry.at}-${index}`))) }));
59
+ }
60
+ export function DeathScreen({ statId = "health", open, className, children, }) {
61
+ const dead = useLocalPlayerDead(statId);
62
+ return (_jsx(Screen, { id: "death", open: open ?? dead, className: className, children: children }));
63
+ }
64
+ export function LevelUpFlash({ stat, durationMs = 1600, className, children, renderFlash, }) {
65
+ const ctx = useGameContext();
66
+ const [flash, setFlash] = useState(null);
67
+ const timerRef = useRef(null);
68
+ useEffect(() => {
69
+ const unsubscribe = ctx.game.events.on("stat.levelUp", (event) => {
70
+ if (stat !== undefined && event.stat !== stat)
71
+ return;
72
+ setFlash(event);
73
+ if (timerRef.current !== null)
74
+ clearTimeout(timerRef.current);
75
+ timerRef.current = setTimeout(() => setFlash(null), durationMs);
76
+ });
77
+ return () => {
78
+ unsubscribe();
79
+ if (timerRef.current !== null)
80
+ clearTimeout(timerRef.current);
81
+ };
82
+ }, [ctx, stat, durationMs]);
83
+ if (flash === null)
84
+ return null;
85
+ return (_jsx("div", { className: className, "data-levelup": flash.level, children: renderFlash !== undefined ? renderFlash(flash) : children ?? `Level ${flash.level}` }));
86
+ }
@@ -0,0 +1,10 @@
1
+ export interface ReadableEngineStore<TState> {
2
+ getState(): TState;
3
+ subscribe(listener: (state: TState) => void): () => void;
4
+ }
5
+ export interface EventfulEngineStore<TEventMap extends object> {
6
+ on<K extends keyof TEventMap>(eventName: K, listener: (payload: TEventMap[K]) => void): () => void;
7
+ }
8
+ export declare function useEngineState<TState>(store: ReadableEngineStore<TState>): TState;
9
+ export declare function useEngineStore<TState, TSelected>(store: ReadableEngineStore<TState>, selector: (state: TState) => TSelected): TSelected;
10
+ export declare function useEngineEvent<TEventMap extends object, K extends keyof TEventMap>(store: EventfulEngineStore<TEventMap>, eventName: K, handler: (payload: TEventMap[K]) => void): void;
@@ -0,0 +1,14 @@
1
+ import { useCallback, useEffect, useRef, useSyncExternalStore } from "react";
2
+ export function useEngineState(store) {
3
+ const subscribe = useCallback((onChange) => store.subscribe(() => onChange()), [store]);
4
+ const getSnapshot = useCallback(() => store.getState(), [store]);
5
+ return useSyncExternalStore(subscribe, getSnapshot, getSnapshot);
6
+ }
7
+ export function useEngineStore(store, selector) {
8
+ return selector(useEngineState(store));
9
+ }
10
+ export function useEngineEvent(store, eventName, handler) {
11
+ const handlerRef = useRef(handler);
12
+ handlerRef.current = handler;
13
+ useEffect(() => store.on(eventName, (payload) => handlerRef.current(payload)), [store, eventName]);
14
+ }
package/dist/hooks.d.ts CHANGED
@@ -39,5 +39,6 @@ export declare function useLeaderboard(stat: string, options: {
39
39
  userId: string;
40
40
  value: number;
41
41
  }[];
42
+ export declare function useLocalPlayerDead(healthStatId?: string): boolean;
42
43
  export declare function localPlayerEntity(ctx: GameContext): SceneEntity | null;
43
44
  export declare function useActivePrompt<T extends PositionedPrompt>(prompts?: readonly T[]): T | null;
package/dist/hooks.js CHANGED
@@ -50,6 +50,15 @@ export function usePresence(userId) {
50
50
  export function useLeaderboard(stat, options) {
51
51
  return useGameStore((ctx) => ctx.game.leaderboard.getTop(stat, options));
52
52
  }
53
+ export function useLocalPlayerDead(healthStatId = "health") {
54
+ return useGameStore((ctx) => {
55
+ const player = localPlayerEntity(ctx);
56
+ if (player === null)
57
+ return false;
58
+ const health = ctx.scene.entity.stats.get(player.id, healthStatId);
59
+ return health !== null && health.current <= health.min;
60
+ });
61
+ }
53
62
  export function localPlayerEntity(ctx) {
54
63
  return (ctx.scene.entity.get(ctx.player.userId) ??
55
64
  ctx.scene.entity.list().find((entity) => entity.role === "player") ??
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jgengine/react",
3
- "version": "0.3.0",
3
+ "version": "0.5.0",
4
4
  "description": "React UI layer for JGengine: GameProvider, hooks, and headless primitives over @jgengine/core.",
5
5
  "license": "AGPL-3.0-only",
6
6
  "type": "module",
@@ -10,7 +10,10 @@
10
10
  "url": "git+https://github.com/Noisemaker111/jgengine.git",
11
11
  "directory": "packages/react"
12
12
  },
13
- "files": ["dist"],
13
+ "files": [
14
+ "dist",
15
+ "CHANGELOG.md"
16
+ ],
14
17
  "exports": {
15
18
  "./*": {
16
19
  "types": "./dist/*.d.ts",
@@ -18,11 +21,11 @@
18
21
  }
19
22
  },
20
23
  "scripts": {
21
- "build": "tsc -p tsconfig.build.json && bun ../../scripts/fix-extensions.ts dist",
22
- "check-types": "tsc --noEmit -p tsconfig.json"
24
+ "build": "tsgo -p tsconfig.build.json && bun ../../scripts/fix-extensions.ts dist",
25
+ "check-types": "tsgo --noEmit -p tsconfig.json"
23
26
  },
24
27
  "dependencies": {
25
- "@jgengine/core": "^0.3.0"
28
+ "@jgengine/core": "^0.5.0"
26
29
  },
27
30
  "peerDependencies": {
28
31
  "react": "^19.0.0"