@jgengine/core 0.4.0 → 0.6.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.
Files changed (51) hide show
  1. package/CHANGELOG.md +66 -0
  2. package/dist/game/defineGame.d.ts +3 -0
  3. package/dist/game/feed.d.ts +2 -0
  4. package/dist/game/feed.js +10 -8
  5. package/dist/game/playableGame.d.ts +10 -1
  6. package/dist/game/progression.d.ts +61 -0
  7. package/dist/game/progression.js +96 -0
  8. package/dist/game/quest.d.ts +39 -0
  9. package/dist/game/quest.js +195 -0
  10. package/dist/game/trade.d.ts +23 -0
  11. package/dist/game/trade.js +50 -8
  12. package/dist/game/unlocks.d.ts +9 -0
  13. package/dist/game/unlocks.js +19 -1
  14. package/dist/inventory/slotModel.d.ts +19 -0
  15. package/dist/inventory/slotModel.js +76 -0
  16. package/dist/meta/changelog.d.ts +8 -0
  17. package/dist/meta/changelog.js +43 -0
  18. package/dist/physics/physicsWorld.d.ts +136 -0
  19. package/dist/physics/physicsWorld.js +489 -0
  20. package/dist/runtime/gameContext.d.ts +2 -0
  21. package/dist/runtime/gameContext.js +3 -0
  22. package/dist/scene/assetCatalog.d.ts +13 -0
  23. package/dist/scene/entityStore.d.ts +6 -0
  24. package/dist/scene/entityStore.js +15 -1
  25. package/dist/time/simClock.d.ts +62 -0
  26. package/dist/time/simClock.js +132 -0
  27. package/dist/world/buildingIndex.d.ts +16 -0
  28. package/dist/world/buildingIndex.js +63 -0
  29. package/dist/world/buildings.d.ts +98 -0
  30. package/dist/world/buildings.js +320 -0
  31. package/dist/world/features.d.ts +105 -1
  32. package/dist/world/features.js +95 -0
  33. package/dist/world/geometry.d.ts +23 -0
  34. package/dist/world/geometry.js +75 -0
  35. package/dist/world/interiors.d.ts +35 -0
  36. package/dist/world/interiors.js +66 -0
  37. package/dist/world/placement.d.ts +30 -0
  38. package/dist/world/placement.js +20 -0
  39. package/dist/world/regions.d.ts +66 -0
  40. package/dist/world/regions.js +158 -0
  41. package/dist/world/scatter.d.ts +24 -0
  42. package/dist/world/scatter.js +126 -0
  43. package/dist/world/scatterItems.d.ts +38 -0
  44. package/dist/world/scatterItems.js +85 -0
  45. package/dist/world/terrain.d.ts +54 -0
  46. package/dist/world/terrain.js +181 -0
  47. package/dist/world/water.d.ts +27 -0
  48. package/dist/world/water.js +125 -0
  49. package/dist/world/wind.d.ts +17 -0
  50. package/dist/world/wind.js +40 -0
  51. package/package.json +5 -2
@@ -2,6 +2,15 @@ export interface UnlockDef {
2
2
  id: string;
3
3
  category?: string;
4
4
  }
5
+ export type UnlockState = readonly string[];
6
+ export declare function hasUnlock(granted: UnlockState, unlockId: string): boolean;
7
+ export declare function grantUnlock(granted: UnlockState, unlockId: string): string[];
8
+ export declare function unlockTree(defs: readonly UnlockDef[], categoryId: string): UnlockDef[];
9
+ export interface UnlockCatalog {
10
+ has(unlockId: string): boolean;
11
+ tree(categoryId: string): UnlockDef[];
12
+ }
13
+ export declare function createUnlockCatalog(defs?: readonly UnlockDef[]): UnlockCatalog;
5
14
  export interface Unlocks {
6
15
  has(userId: string, unlockId: string): boolean;
7
16
  grant(userId: string, unlockId: string): void;
@@ -1,3 +1,21 @@
1
+ export function hasUnlock(granted, unlockId) {
2
+ return granted.includes(unlockId);
3
+ }
4
+ export function grantUnlock(granted, unlockId) {
5
+ return granted.includes(unlockId) ? granted.slice() : [...granted, unlockId];
6
+ }
7
+ export function unlockTree(defs, categoryId) {
8
+ return defs.filter((def) => def.category === categoryId);
9
+ }
10
+ export function createUnlockCatalog(defs = []) {
11
+ const byId = new Map();
12
+ for (const def of defs)
13
+ byId.set(def.id, def);
14
+ return {
15
+ has: (unlockId) => byId.has(unlockId),
16
+ tree: (categoryId) => unlockTree(defs, categoryId),
17
+ };
18
+ }
1
19
  export function createUnlocks(defs = []) {
2
20
  const definitions = new Map();
3
21
  for (const def of defs)
@@ -22,7 +40,7 @@ export function createUnlocks(defs = []) {
22
40
  return Array.from(granted.get(userId) ?? []);
23
41
  },
24
42
  tree(categoryId) {
25
- return Array.from(definitions.values()).filter((def) => def.category === categoryId);
43
+ return unlockTree(Array.from(definitions.values()), categoryId);
26
44
  },
27
45
  snapshot(userId) {
28
46
  return Array.from(granted.get(userId) ?? []);
@@ -0,0 +1,19 @@
1
+ export type Slot<T> = T | null;
2
+ export type SlotGrid<T> = readonly Slot<T>[];
3
+ export type SlotResult<T> = {
4
+ status: "ok";
5
+ grid: Slot<T>[];
6
+ } | {
7
+ status: "rejected";
8
+ reason: "invalid-slot" | "slot-occupied" | "empty-slot";
9
+ };
10
+ export declare function createSlots<T>(size: number): Slot<T>[];
11
+ export declare function normalizeSlots<T>(values: SlotGrid<T>, size: number): Slot<T>[];
12
+ export declare function placeAt<T>(grid: SlotGrid<T>, index: number, value: T): SlotResult<T>;
13
+ export declare function removeAt<T>(grid: SlotGrid<T>, index: number): SlotResult<T>;
14
+ export declare function moveSlot<T>(grid: SlotGrid<T>, from: number, to: number): SlotResult<T>;
15
+ export declare function firstEmpty<T>(grid: SlotGrid<T>): number | null;
16
+ export declare function insertFirst<T>(grid: SlotGrid<T>, value: T): SlotResult<T>;
17
+ export declare function compactSlots<T>(grid: SlotGrid<T>): Slot<T>[];
18
+ export declare function countFilled<T>(grid: SlotGrid<T>): number;
19
+ export declare function indexOfSlot<T>(grid: SlotGrid<T>, match: (value: T, index: number) => boolean): number | null;
@@ -0,0 +1,76 @@
1
+ export function createSlots(size) {
2
+ return new Array(Math.max(0, size)).fill(null);
3
+ }
4
+ export function normalizeSlots(values, size) {
5
+ const grid = createSlots(size);
6
+ for (let index = 0; index < size; index++)
7
+ grid[index] = values[index] ?? null;
8
+ return grid;
9
+ }
10
+ function inRange(grid, index) {
11
+ return Number.isInteger(index) && index >= 0 && index < grid.length;
12
+ }
13
+ export function placeAt(grid, index, value) {
14
+ if (!inRange(grid, index))
15
+ return { status: "rejected", reason: "invalid-slot" };
16
+ if (grid[index] !== null)
17
+ return { status: "rejected", reason: "slot-occupied" };
18
+ const next = grid.slice();
19
+ next[index] = value;
20
+ return { status: "ok", grid: next };
21
+ }
22
+ export function removeAt(grid, index) {
23
+ if (!inRange(grid, index))
24
+ return { status: "rejected", reason: "invalid-slot" };
25
+ if (grid[index] === null)
26
+ return { status: "rejected", reason: "empty-slot" };
27
+ const next = grid.slice();
28
+ next[index] = null;
29
+ return { status: "ok", grid: next };
30
+ }
31
+ export function moveSlot(grid, from, to) {
32
+ if (!inRange(grid, from) || !inRange(grid, to))
33
+ return { status: "rejected", reason: "invalid-slot" };
34
+ if (grid[from] === null)
35
+ return { status: "rejected", reason: "empty-slot" };
36
+ const next = grid.slice();
37
+ const moved = next[from];
38
+ next[from] = next[to];
39
+ next[to] = moved;
40
+ return { status: "ok", grid: next };
41
+ }
42
+ export function firstEmpty(grid) {
43
+ for (let index = 0; index < grid.length; index++) {
44
+ if (grid[index] === null)
45
+ return index;
46
+ }
47
+ return null;
48
+ }
49
+ export function insertFirst(grid, value) {
50
+ const index = firstEmpty(grid);
51
+ if (index === null)
52
+ return { status: "rejected", reason: "slot-occupied" };
53
+ return placeAt(grid, index, value);
54
+ }
55
+ export function compactSlots(grid) {
56
+ const filled = grid.filter((slot) => slot !== null);
57
+ const next = createSlots(grid.length);
58
+ for (let index = 0; index < filled.length; index++)
59
+ next[index] = filled[index];
60
+ return next;
61
+ }
62
+ export function countFilled(grid) {
63
+ let count = 0;
64
+ for (const slot of grid)
65
+ if (slot !== null)
66
+ count += 1;
67
+ return count;
68
+ }
69
+ export function indexOfSlot(grid, match) {
70
+ for (let index = 0; index < grid.length; index++) {
71
+ const slot = grid[index];
72
+ if (slot !== null && match(slot, index))
73
+ return index;
74
+ }
75
+ return null;
76
+ }
@@ -0,0 +1,8 @@
1
+ export declare const VERSION = "0.6.0";
2
+ export interface ChangelogEntry {
3
+ migrate: readonly string[];
4
+ added: readonly string[];
5
+ changed: readonly string[];
6
+ removed: readonly string[];
7
+ }
8
+ export declare const CHANGELOG: Record<string, ChangelogEntry>;
@@ -0,0 +1,43 @@
1
+ export const VERSION = "0.6.0";
2
+ export const CHANGELOG = {
3
+ "0.6.0": {
4
+ migrate: [
5
+ "Bump every @jgengine/* dependency to ^0.6.0 (the eight packages version in lockstep).",
6
+ "0.6.0 is additive — every 0.5.0 API is unchanged, so no code change is required to upgrade.",
7
+ "Optional: mount an outdoor scene by handing environment({ terrain, weather, vegetation, water, structures }) from @jgengine/core/world/features to the shell — @jgengine/shell EnvironmentScene renders terrain, rain/snow, grass, ocean, and buildings from it, while gameplay reads the same world through the renderer-free query primitives (resolveTerrainField, windField, waterSurface, scatter, buildingIndex).",
8
+ ],
9
+ added: [
10
+ "@jgengine/core/world query primitives — renderer-free world sampling so gameplay reads the same world the shell renders: world/terrain (TerrainField, noiseField, resolveTerrainField, resolveGroundStep), world/wind (windField), world/water (waterSurface, waterSurfaceFromDescriptor, CPU Gerstner), world/scatter (scatter — seeded overlap-aware point distribution), world/buildings (generateBuilding, generateBuildingDistrict) and world/buildingIndex (buildingIndex — at/within/nearest/isInside/blockers).",
11
+ "@jgengine/core/world/features gained composable outdoor descriptors: environment(), terrain(), rain(), snow(), grass(), ocean(), building().",
12
+ "@jgengine/core/world/regions (createRegionField — blend content-agnostic biomes by nearest selector, extends TerrainField so it ground-snaps) and world/scatterItems (scatterItems — region-driven, grounded, above-water/slope-aware content scatter with pickWeighted).",
13
+ "@jgengine/core/physics/physicsWorld — standalone fixed-capacity rigid-body sim (SoA buffers, spatial-hash broadphase, sleeping) for many colliding dynamic bodies; independent of the defineGame physics character-controller config.",
14
+ "ctx.time simulation clock (@jgengine/core/time/simClock) — dt in onTick is game time, so decay/regen/AI written as rate*dt obey pause and fast-forward for free. Configure via defineGame({ time: { scale?, speeds?, dayLength?, start?, startPaused? } }); pause/play/setSpeed/calendar controls, and useGameClock() in React.",
15
+ "@jgengine/shell environment/terrain/water/weather/structures renderers — EnvironmentScene mounts an environment() descriptor; GrassField, ProceduralGround, Ocean, RainField, SnowField, LightningStrike, GeneratedBuilding, and world/InstancedBodies (renders PhysicsWorld bodies).",
16
+ "@jgengine/react/liveBind — useFrameBind drives a DOM/SVG element from a per-frame value without re-rendering React.",
17
+ ],
18
+ changed: [],
19
+ removed: [],
20
+ },
21
+ "0.5.0": {
22
+ migrate: [
23
+ "Bump every @jgengine/* dependency to ^0.5.0 (the eight packages version in lockstep).",
24
+ "0.5.0 is additive — all 0.4.0 APIs are unchanged, so no code change is required to upgrade.",
25
+ "Optional: replace a game's hand-rolled progression/curves.ts with leveling({ xpForLevel: { kind: 'power', base, exponent, round: 'floor' }, maxLevel }) from @jgengine/core/game/progression; its xpForLevel/resolve/grantXp are drop-ins and ctx.scene.entity.stats satisfies LevelingStatAccess with no adapter.",
26
+ ],
27
+ added: [
28
+ "@jgengine/core/game/progression — declarative scalar curves (curve/evalCurve: const, linear, power, geometric, steps, piecewise with round + min/max) and a leveling() XP->level track built on an xpForLevel curve.",
29
+ "@jgengine/core/inventory/slotModel — pure slot-grid primitives (createSlots, placeAt, removeAt, moveSlot).",
30
+ "@jgengine/core/world/geometry, /world/interiors, /world/placement — pure world primitives: grid snapping, footprint AABBs/overlap, interior/exterior spaces, and placement validation.",
31
+ "@jgengine/react/engineStore — raw-store React bindings (useEngineState, useEngineStore, useEngineEvent).",
32
+ "Pure/functional tiers for the trade, unlocks, quest, and feed verbs in @jgengine/core/game.",
33
+ ],
34
+ changed: [],
35
+ removed: [],
36
+ },
37
+ "0.4.0": {
38
+ migrate: [],
39
+ added: ["Baseline release: core, ws, sql, react, convex, node, shell, assets."],
40
+ changed: [],
41
+ removed: [],
42
+ },
43
+ };
@@ -0,0 +1,136 @@
1
+ export interface PhysicsBounds {
2
+ min: readonly [number, number, number];
3
+ max: readonly [number, number, number];
4
+ }
5
+ export interface PhysicsWorldConfig {
6
+ /** Maximum bodies the SoA buffers can hold. Fixed at construction; no reallocation later. */
7
+ capacity: number;
8
+ /** AABB container; every body is constrained inside it (floor included). */
9
+ bounds: PhysicsBounds;
10
+ /** Downward acceleration applied to awake bodies each substep. Default -20. */
11
+ gravity?: number;
12
+ /** Broadphase cell edge; tune to ≈ small-body diameter. Default 1. */
13
+ cellSize?: number;
14
+ /** Fixed substep length in seconds. Default 1/60. */
15
+ fixedDt?: number;
16
+ /** Cap on substeps per `step()` — bounds catch-up work, drops the rest. Default 4. */
17
+ maxSubsteps?: number;
18
+ /** Bounce factor on contacts and walls, 0..1. Default 0.2. */
19
+ restitution?: number;
20
+ /** Approach speed below which restitution is ignored (contacts rest instead of jittering). Default 1. */
21
+ restitutionThreshold?: number;
22
+ /** Coulomb friction coefficient on contacts and the floor, 0..1. Default 0.4. */
23
+ friction?: number;
24
+ /** Per-second linear velocity damping (physical energy bleed so piles settle and sleep). Default 0.2. */
25
+ linearDamping?: number;
26
+ /** Sequential-impulse velocity iterations per substep (higher = more stable stacks). Default 4. */
27
+ solverIterations?: number;
28
+ /** Baumgarte positional-correction fraction per substep, 0..1. Default 0.2. */
29
+ correctionFactor?: number;
30
+ /** Penetration tolerated before correction kicks in. Default 0.005. */
31
+ slop?: number;
32
+ /** Speed at or below which a body counts as still. Default 0.08. */
33
+ sleepLinearVelocity?: number;
34
+ /** Consecutive still substeps before a body sleeps. Default 30. */
35
+ sleepThresholdSteps?: number;
36
+ /** Approach speed a contact must exceed to wake a sleeping body (resting contacts don't). Default 0.5. */
37
+ wakeThreshold?: number;
38
+ }
39
+ export interface PhysicsStats {
40
+ count: number;
41
+ awake: number;
42
+ sleeping: number;
43
+ contacts: number;
44
+ pairs: number;
45
+ substeps: number;
46
+ stepMs: number;
47
+ }
48
+ export interface AddBodyOptions {
49
+ position: readonly [number, number, number];
50
+ halfExtents: readonly [number, number, number];
51
+ velocity?: readonly [number, number, number];
52
+ /** ≤ 0 or omitted with `static` → infinite mass (immovable collider). Default 1. */
53
+ mass?: number;
54
+ /** Immovable collider: never integrates, never sleeps-out, invMass 0. */
55
+ static?: boolean;
56
+ /** Seed the body asleep (a settled bed pays no integration cost until woken). */
57
+ asleep?: boolean;
58
+ }
59
+ /** Grid cell containing a point, clamped into the grid. Pure — exported for tests. */
60
+ export declare function cellCoord(value: number, min: number, cellSize: number, cells: number): number;
61
+ /** Linear cell index from clamped 3D coords. Pure — exported for tests. */
62
+ export declare function cellIndex(cx: number, cy: number, cz: number, nx: number, ny: number): number;
63
+ export declare class PhysicsWorld {
64
+ readonly capacity: number;
65
+ readonly bounds: PhysicsBounds;
66
+ readonly gravity: number;
67
+ readonly cellSize: number;
68
+ readonly fixedDt: number;
69
+ readonly maxSubsteps: number;
70
+ readonly restitution: number;
71
+ readonly restitutionThreshold: number;
72
+ readonly friction: number;
73
+ readonly linearDamping: number;
74
+ readonly solverIterations: number;
75
+ readonly correctionFactor: number;
76
+ readonly slop: number;
77
+ readonly sleepMove2: number;
78
+ readonly sleepSteps: number;
79
+ readonly wakeThreshold: number;
80
+ readonly posX: Float32Array;
81
+ readonly posY: Float32Array;
82
+ readonly posZ: Float32Array;
83
+ readonly velX: Float32Array;
84
+ readonly velY: Float32Array;
85
+ readonly velZ: Float32Array;
86
+ readonly halfX: Float32Array;
87
+ readonly halfY: Float32Array;
88
+ readonly halfZ: Float32Array;
89
+ readonly invMass: Float32Array;
90
+ readonly flags: Uint8Array;
91
+ /** 1 for a body that took part in a contact this frame; reset each `step()`. Render tint reads this. */
92
+ readonly contact: Uint8Array;
93
+ private readonly sleepTimer;
94
+ private readonly prevX;
95
+ private readonly prevY;
96
+ private readonly prevZ;
97
+ private readonly nx;
98
+ private readonly ny;
99
+ private readonly nz;
100
+ private readonly numCells;
101
+ private readonly cellStart;
102
+ private readonly cursor;
103
+ private readonly sorted;
104
+ private readonly bodyCell;
105
+ private readonly awakeList;
106
+ private readonly awakeSlot;
107
+ private awakeCount;
108
+ private bodyCount;
109
+ private accumulator;
110
+ private readonly stats;
111
+ constructor(config: PhysicsWorldConfig);
112
+ private addAwake;
113
+ private removeAwake;
114
+ get count(): number;
115
+ get cells(): {
116
+ nx: number;
117
+ ny: number;
118
+ nz: number;
119
+ total: number;
120
+ };
121
+ addBody(options: AddBodyOptions): number;
122
+ isSleeping(i: number): boolean;
123
+ wake(i: number): void;
124
+ wakeAll(): void;
125
+ clear(): void;
126
+ /** Advance by a real frame delta; runs whole fixed substeps and carries the remainder. */
127
+ step(frameDt: number): PhysicsStats;
128
+ getStats(): PhysicsStats;
129
+ private substep;
130
+ private constrainBounds;
131
+ private buildGrid;
132
+ private cellOf;
133
+ private solve;
134
+ private resolve;
135
+ private updateSleep;
136
+ }