@jgengine/shell 0.5.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 (62) hide show
  1. package/CHANGELOG.md +24 -0
  2. package/README.md +27 -0
  3. package/dist/GamePlayerShell.js +19 -9
  4. package/dist/demo/demoGame.js +10 -2
  5. package/dist/demo/environmentShowcase.d.ts +2 -0
  6. package/dist/demo/environmentShowcase.js +15 -0
  7. package/dist/environment/EnvironmentScene.d.ts +5 -0
  8. package/dist/environment/EnvironmentScene.js +75 -0
  9. package/dist/environment/index.d.ts +1 -0
  10. package/dist/environment/index.js +1 -0
  11. package/dist/environment.d.ts +1 -0
  12. package/dist/environment.js +1 -0
  13. package/dist/registry.d.ts +4 -2
  14. package/dist/structures/GeneratedBuilding.d.ts +44 -0
  15. package/dist/structures/GeneratedBuilding.js +86 -0
  16. package/dist/terrain/GrassField.d.ts +21 -0
  17. package/dist/terrain/GrassField.js +31 -0
  18. package/dist/terrain/ProceduralGround.d.ts +9 -0
  19. package/dist/terrain/ProceduralGround.js +17 -0
  20. package/dist/terrain/grassGeometry.d.ts +27 -0
  21. package/dist/terrain/grassGeometry.js +77 -0
  22. package/dist/terrain/grassMaterial.d.ts +33 -0
  23. package/dist/terrain/grassMaterial.js +108 -0
  24. package/dist/terrain/index.d.ts +7 -0
  25. package/dist/terrain/index.js +7 -0
  26. package/dist/terrain/random.d.ts +4 -0
  27. package/dist/terrain/random.js +27 -0
  28. package/dist/terrain/terrainMath.d.ts +34 -0
  29. package/dist/terrain/terrainMath.js +92 -0
  30. package/dist/terrain.d.ts +1 -0
  31. package/dist/terrain.js +1 -0
  32. package/dist/water/Ocean.d.ts +6 -0
  33. package/dist/water/Ocean.js +30 -0
  34. package/dist/water/OceanConfig.d.ts +90 -0
  35. package/dist/water/OceanConfig.js +131 -0
  36. package/dist/water/OceanMaterial.d.ts +54 -0
  37. package/dist/water/OceanMaterial.js +51 -0
  38. package/dist/water/OceanShader.d.ts +2 -0
  39. package/dist/water/OceanShader.js +78 -0
  40. package/dist/water/index.d.ts +3 -0
  41. package/dist/water/index.js +3 -0
  42. package/dist/water.d.ts +1 -0
  43. package/dist/water.js +1 -0
  44. package/dist/weather/LightningStrike.d.ts +17 -0
  45. package/dist/weather/LightningStrike.js +130 -0
  46. package/dist/weather/RainField.d.ts +20 -0
  47. package/dist/weather/RainField.js +107 -0
  48. package/dist/weather/SnowField.d.ts +19 -0
  49. package/dist/weather/SnowField.js +108 -0
  50. package/dist/weather/WeatherLayer.d.ts +17 -0
  51. package/dist/weather/WeatherLayer.js +16 -0
  52. package/dist/weather/index.d.ts +5 -0
  53. package/dist/weather/index.js +5 -0
  54. package/dist/weather/weatherGeometry.d.ts +2 -0
  55. package/dist/weather/weatherGeometry.js +14 -0
  56. package/dist/weather/weatherMath.d.ts +7 -0
  57. package/dist/weather/weatherMath.js +30 -0
  58. package/dist/weather/weatherUniforms.d.ts +18 -0
  59. package/dist/weather/weatherUniforms.js +34 -0
  60. package/dist/world/InstancedBodies.d.ts +17 -0
  61. package/dist/world/InstancedBodies.js +84 -0
  62. package/package.json +8 -4
package/CHANGELOG.md CHANGED
@@ -15,6 +15,30 @@ the latest and surface the migration steps.
15
15
 
16
16
  _Nothing yet._
17
17
 
18
+ ## 0.6.0
19
+
20
+ An additive release: **every 0.5.0 API is unchanged**, so upgrading is only a
21
+ version bump. The headline is a whole outdoor-world layer — composable
22
+ `environment(...)` descriptors, renderer-free query primitives so gameplay reads
23
+ the same world the shell renders, a standalone rigid-body physics sim, and the
24
+ `@jgengine/shell` renderers that draw it all.
25
+
26
+ ### Migrate
27
+
28
+ - Bump every `@jgengine/*` dependency to `^0.6.0` (the eight packages version in lockstep).
29
+ - No code change is required — 0.6.0 adds surface, it doesn't move or remove any.
30
+ - Optional: describe an outdoor scene once with `environment({ terrain, weather, vegetation, water, structures })` from `@jgengine/core/world/features`. `@jgengine/shell`'s `EnvironmentScene` renders it (terrain, rain/snow, grass, ocean, buildings), and gameplay reads the same world through the renderer-free primitives — `resolveTerrainField(...)` for ground-snap/collision, `windField(...)` for sway/sailing, `waterSurface(...)` for buoyancy, `scatter(...)` for prop/spawn placement, `buildingIndex(...)` for placement avoidance — no three.js needed.
31
+
32
+ ### Added
33
+
34
+ - `@jgengine/core/world` query primitives — pure, renderer-free world sampling so gameplay and rendering read one source of truth. `world/terrain` (`TerrainField`, `noiseField`, `resolveTerrainField`, `resolveGroundStep` for slope-limited movement), `world/wind` (`windField` — one wind source for weather sway, grass, sailing, fire spread), `world/water` (`waterSurface` / `waterSurfaceFromDescriptor` — CPU Gerstner matching the ocean shader, for buoyancy and shorelines), `world/scatter` (`scatter` — seeded, overlap-aware point distribution for vegetation/props/spawns), `world/buildings` (`generateBuilding`, `generateBuildingDistrict`) and `world/buildingIndex` (`buildingIndex` — `at`/`within`/`nearest`/`isInside`/`blockers` over a district).
35
+ - `@jgengine/core/world/features` composable outdoor descriptors — `environment(...)` plus `terrain()`, `rain()`, `snow()`, `grass()`, `ocean()`, `building()`.
36
+ - `@jgengine/core/world/regions` (`createRegionField` — blend content-agnostic biomes by nearest selector, adding `tint`/`water`/`fog`/`speedMultiplier`/opaque `data`; extends `TerrainField` so it ground-snaps too) and `@jgengine/core/world/scatterItems` (`scatterItems` — region-driven content scatter: density per region, grounded, above-water/slope-aware, with `pickWeighted` for weighted rolls).
37
+ - `@jgengine/core/physics/physicsWorld` — standalone fixed-capacity rigid-body sim (SoA buffers, spatial-hash broadphase, sleeping) for many colliding dynamic bodies (piles, debris, stress scenes). Distinct from the `defineGame` `physics: { gravity }` character-controller config.
38
+ - `ctx.time` simulation clock (`@jgengine/core/time/simClock`) — `onTick`'s `dt` is now **game time**, so `rate * dt` decay/regen/AI obeys pause and fast-forward for free without per-system wiring. Configure with `defineGame({ time: { scale?, speeds?, dayLength?, start?, startPaused? } })` (all optional; default is real-time 1:1). `ctx.time` exposes `pause`/`play`/`toggle`/`setSpeed`/`cycleSpeed` + `snapshot()`/`calendar()`; `useGameClock()` binds it in React.
39
+ - `@jgengine/shell` environment/terrain/water/weather/structures renderers — `EnvironmentScene` mounts an `environment()` descriptor as R3F renderers; `GrassField`, `ProceduralGround`, `Ocean`, `RainField`, `SnowField`, `LightningStrike`, `GeneratedBuilding`, and `world/InstancedBodies` (renders `PhysicsWorld` bodies).
40
+ - `@jgengine/react/liveBind` — `useFrameBind` drives a DOM/SVG element from a per-frame value without re-rendering React (HUDs bound to live engine state never re-render or lag).
41
+
18
42
  ## 0.5.0
19
43
 
20
44
  An additive release: **every 0.4.0 API is unchanged**, so upgrading is only a
package/README.md CHANGED
@@ -14,3 +14,30 @@ const games: GameRegistry = {
14
14
  ```
15
15
 
16
16
  Peer deps: `react`, `three`, `@react-three/fiber`, `@react-three/drei`, `three-stdlib`. The shell's HUD classes are Tailwind — add an `@source` entry for `node_modules/@jgengine/shell` in your CSS. AGPL-3.0-only.
17
+
18
+ Weather primitives are available from `@jgengine/shell/weather` for game scene overlays:
19
+
20
+ ```tsx
21
+ import { WeatherLayer, LightningStrike } from "@jgengine/shell/weather";
22
+
23
+ <WeatherLayer mode="rain" intensity={0.8} wind={[1.5, 0, -0.4]} rain={{ count: 7000 }} />;
24
+ <LightningStrike origin={[0, 22, 0]} target={[4, 0, -6]} strikeKey={stormTick} />;
25
+ ```
26
+
27
+ `RainField` and `SnowField` can also be mounted directly. The primitives use camera-following instanced volumes, prop-driven density controls, shared time/wind uniforms under `WeatherLayer`, and explicit Three resource disposal. They were shaped from ideas in achrefelouafi's MIT RainSystemThreeJS and SnowSystemThreeJS references without bringing over GUI, audio, postprocessing, or app-specific scene setup.
28
+
29
+ ## Terrain primitives
30
+
31
+ `@jgengine/shell/terrain` exports R3F helpers for shell-local natural scenes:
32
+
33
+ ```tsx
34
+ import { GrassField, ProceduralGround, createProceduralTerrainSampler } from "@jgengine/shell/terrain";
35
+
36
+ const terrain = { seed: "meadow", size: 48, height: 0.9 };
37
+ const heightAt = createProceduralTerrainSampler(terrain);
38
+
39
+ <>
40
+ <ProceduralGround terrain={terrain} />
41
+ <GrassField area={48} heightAt={heightAt} density={0.65} />
42
+ </>;
43
+ ```
@@ -85,23 +85,31 @@ function EntityModel({ model }) {
85
85
  const gltf = useLoader(GLTFLoader, model.url);
86
86
  const scene = useMemo(() => gltf.scene.clone(true), [gltf]);
87
87
  const scale = model.scale ?? 1;
88
- return _jsx("primitive", { object: scene, "position-y": model.y ?? 0, scale: [scale, scale, scale] });
88
+ const baseY = model.y ?? 0;
89
+ const dims = model.dims;
90
+ const centered = (model.anchor ?? "center") === "center" && dims !== undefined;
91
+ const position = centered
92
+ ? [-scale * dims.center.x, baseY - scale * dims.minY, -scale * dims.center.z]
93
+ : [0, baseY, 0];
94
+ return _jsx("primitive", { object: scene, position: position, scale: [scale, scale, scale] });
89
95
  }
90
96
  function resolveModel(value, assets) {
91
97
  if (value === undefined)
92
98
  return undefined;
93
99
  if (typeof value !== "string")
94
100
  return value;
95
- const url = assets.resolve(value)?.url;
96
- return url === undefined ? undefined : { url };
101
+ const ref = assets.resolve(value);
102
+ if (ref === null)
103
+ return undefined;
104
+ return ref.dims === undefined ? { url: ref.url } : { url: ref.url, dims: ref.dims };
97
105
  }
98
- function EntityMarker({ entity, model, sprite, isLocal, targeted, onSelect, }) {
106
+ function EntityMarker({ entity, custom, model, sprite, isLocal, targeted, onSelect, }) {
99
107
  const color = isLocal ? "#4ade80" : entity.role === "npc" ? colorFromId(entity.name) : "#9ca3af";
100
108
  return (_jsxs("group", { position: [entity.position[0], entity.position[1], entity.position[2]], "rotation-y": entity.rotationY, onPointerDown: (event) => {
101
109
  event.stopPropagation();
102
110
  if (!isLocal)
103
111
  onSelect(entity);
104
- }, children: [model !== undefined ? (_jsx(EntityModel, { model: model })) : sprite !== undefined ? (_jsx(EntitySprite, { sprite: sprite })) : entity.role === "prop" ? (_jsxs("mesh", { "position-y": 0.5, children: [_jsx("sphereGeometry", { args: [0.45, 16, 16] }), _jsx("meshStandardMaterial", { color: color })] })) : (_jsxs(_Fragment, { children: [_jsxs("mesh", { "position-y": 0.95, children: [_jsx("capsuleGeometry", { args: [0.35, 1.1, 6, 14] }), _jsx("meshStandardMaterial", { color: color })] }), _jsxs("mesh", { position: [0, 1.35, 0.32], children: [_jsx("boxGeometry", { args: [0.16, 0.16, 0.16] }), _jsx("meshStandardMaterial", { color: "#f8fafc" })] })] })), targeted ? (_jsxs("mesh", { "rotation-x": -Math.PI / 2, "position-y": 0.03, children: [_jsx("ringGeometry", { args: [0.6, 0.75, 28] }), _jsx("meshBasicMaterial", { color: "#f87171" })] })) : null] }));
112
+ }, children: [custom !== undefined && custom !== null ? (custom) : model !== undefined ? (_jsx(EntityModel, { model: model })) : sprite !== undefined ? (_jsx(EntitySprite, { sprite: sprite })) : entity.role === "prop" ? (_jsxs("mesh", { "position-y": 0.5, children: [_jsx("sphereGeometry", { args: [0.45, 16, 16] }), _jsx("meshStandardMaterial", { color: color })] })) : (_jsxs(_Fragment, { children: [_jsxs("mesh", { "position-y": 0.95, children: [_jsx("capsuleGeometry", { args: [0.35, 1.1, 6, 14] }), _jsx("meshStandardMaterial", { color: color })] }), _jsxs("mesh", { position: [0, 1.35, 0.32], children: [_jsx("boxGeometry", { args: [0.16, 0.16, 0.16] }), _jsx("meshStandardMaterial", { color: "#f8fafc" })] })] })), targeted ? (_jsxs("mesh", { "rotation-x": -Math.PI / 2, "position-y": 0.03, children: [_jsx("ringGeometry", { args: [0.6, 0.75, 28] }), _jsx("meshBasicMaterial", { color: "#f87171" })] })) : null] }));
105
113
  }
106
114
  function GroundPlane() {
107
115
  const geometry = useMemo(() => {
@@ -134,7 +142,7 @@ function RockField() {
134
142
  }), []);
135
143
  return (_jsx(_Fragment, { children: rocks.map((rock) => (_jsxs("mesh", { position: [rock.x, 0.25 * rock.scale, rock.z], rotation: [0.1, rock.rotation, -0.08], scale: [rock.scale * 1.4, rock.scale * 0.7, rock.scale], children: [_jsx("dodecahedronGeometry", { args: [0.8, 0] }), _jsx("meshStandardMaterial", { color: "#6b6f63", roughness: 1 })] }, rock.id))) }));
136
144
  }
137
- function WorldView({ entitySprites, entityModels, objectModels, assets, }) {
145
+ function WorldView({ entitySprites, entityModels, objectModels, environment: Environment, assets, renderEntity, }) {
138
146
  const ctx = useGameContext();
139
147
  const entities = useSceneEntities();
140
148
  const objects = useSceneObjects();
@@ -144,7 +152,7 @@ function WorldView({ entitySprites, entityModels, objectModels, assets, }) {
144
152
  const relation = ctx.scene.entity.canReceive(entity.id, "damage") === null ? "hostile" : "friendly";
145
153
  ctx.scene.entity.setTarget(player.userId, relation === "hostile" || entity.role === "npc" ? entity.id : null);
146
154
  };
147
- return (_jsxs(_Fragment, { children: [_jsx(GroundPlane, {}), _jsx("gridHelper", { args: [160, 80, "#3a3f4a", "#2b2f38"], "position-y": 0.01 }), _jsx(RockField, {}), entities.map((entity) => (_jsx(EntityMarker, { entity: entity, model: resolveModel(entityModels?.[entity.name], assets), sprite: entitySprites?.[entity.name], isLocal: entity.id === player.userId, targeted: entity.id === targetId, onSelect: handleSelect }, entity.id))), objects.map((object) => {
155
+ return (_jsxs(_Fragment, { children: [Environment !== undefined ? (_jsx(Environment, {})) : (_jsxs(_Fragment, { children: [_jsx(GroundPlane, {}), _jsx("gridHelper", { args: [160, 80, "#3a3f4a", "#2b2f38"], "position-y": 0.01 }), _jsx(RockField, {})] })), entities.map((entity) => (_jsx(EntityMarker, { entity: entity, custom: renderEntity?.(entity), model: resolveModel(entityModels?.[entity.name], assets), sprite: entitySprites?.[entity.name], isLocal: entity.id === player.userId, targeted: entity.id === targetId, onSelect: handleSelect }, entity.id))), objects.map((object) => {
148
156
  const model = resolveModel(objectModels?.[object.catalogId], assets) ??
149
157
  resolveModel(object.catalogId, assets);
150
158
  return (_jsx("group", { position: [object.position[0], object.position[1], object.position[2]], "rotation-y": object.rotationY, children: model !== undefined ? (_jsx(EntityModel, { model: model })) : (_jsxs("mesh", { "position-y": 0.5, children: [_jsx("boxGeometry", { args: [1, 1, 1] }), _jsx("meshStandardMaterial", { color: colorFromId(object.catalogId) })] })) }, object.instanceId));
@@ -161,6 +169,7 @@ function FrameDriver({ ctx, playable, tracker, yawRef, pitchRef, primaryClickRef
161
169
  useFrame((_state, rawDt) => {
162
170
  try {
163
171
  const dt = Math.min(rawDt, 0.05);
172
+ const gameDt = ctx.time.advance(dt);
164
173
  if (tracker.isDown("turnLeft"))
165
174
  yawRef.current += TURN_SPEED * dt;
166
175
  if (tracker.isDown("turnRight"))
@@ -185,9 +194,10 @@ function FrameDriver({ ctx, playable, tracker, yawRef, pitchRef, primaryClickRef
185
194
  rotationY: intent.moving
186
195
  ? Math.atan2(motion.horizontalVelocityX, motion.horizontalVelocityZ)
187
196
  : player.rotationY,
197
+ dt: rawDt,
188
198
  });
189
199
  }
190
- playable.loop.onTick(ctx, dt);
200
+ playable.loop.onTick(ctx, gameDt);
191
201
  if (tracker.wasPressed("tabTarget")) {
192
202
  if (ctx.game.commands.has("target.cycle"))
193
203
  ctx.game.commands.run("target.cycle", {});
@@ -430,7 +440,7 @@ export function GamePlayerShell({ playable, multiplayer = null, }) {
430
440
  else if (event.deltaY > 0 && ctx.game.commands.has("ui.hotbarScrollPrev")) {
431
441
  ctx.game.commands.run("ui.hotbarScrollPrev", {});
432
442
  }
433
- }, children: [_jsxs(Canvas, { camera: { fov: 55, near: 0.1, far: 300 }, children: [_jsx("color", { attach: "background", args: ["#14161b"] }), _jsx("ambientLight", { intensity: 0.55 }), _jsx("directionalLight", { position: [10, 16, 6], intensity: 1.3 }), _jsxs(GameProvider, { context: ctx, children: [_jsx(WorldView, { entitySprites: playable.entitySprites, entityModels: playable.entityModels, objectModels: playable.objectModels, assets: playable.game.assets }), WorldOverlay !== undefined ? _jsx(WorldOverlay, {}) : null, barsStatId !== null ? _jsx(WorldEntityBars, { statId: barsStatId }) : null, _jsx(WorldFloatText, {}), _jsx(ProjectileTracers, {}), firstPerson ? (_jsx(GameFirstPersonCamera, { yawRef: yawRef, pitchRef: pitchRef, config: playable.camera?.firstPerson, followEntityId: playable.camera?.followEntityId })) : (_jsx(GameOrbitCamera, { yawRef: yawRef, pitchRef: pitchRef, config: playable.camera, followEntityId: playable.camera?.followEntityId, onCameraFollow: playable.camera?.onCameraFollow, onDragChange: (dragging) => {
443
+ }, children: [_jsxs(Canvas, { camera: { fov: 55, near: 0.1, far: 300 }, children: [_jsx("color", { attach: "background", args: ["#14161b"] }), _jsx("ambientLight", { intensity: 0.55 }), _jsx("directionalLight", { position: [10, 16, 6], intensity: 1.3 }), _jsxs(GameProvider, { context: ctx, children: [_jsx(WorldView, { entitySprites: playable.entitySprites, entityModels: playable.entityModels, objectModels: playable.objectModels, environment: playable.environment, assets: playable.game.assets, renderEntity: playable.renderEntity }), WorldOverlay !== undefined ? _jsx(WorldOverlay, {}) : null, barsStatId !== null ? _jsx(WorldEntityBars, { statId: barsStatId }) : null, _jsx(WorldFloatText, {}), _jsx(ProjectileTracers, {}), firstPerson ? (_jsx(GameFirstPersonCamera, { yawRef: yawRef, pitchRef: pitchRef, config: playable.camera?.firstPerson, followEntityId: playable.camera?.followEntityId })) : (_jsx(GameOrbitCamera, { yawRef: yawRef, pitchRef: pitchRef, config: playable.camera, followEntityId: playable.camera?.followEntityId, onCameraFollow: playable.camera?.onCameraFollow, onDragChange: (dragging) => {
434
444
  cameraDraggingRef.current = dragging;
435
445
  } }))] }), _jsx(RemotePlayers, { rows: remotePlayers }), _jsx(FrameDriver, { ctx: ctx, playable: playable, tracker: tracker, yawRef: yawRef, pitchRef: pitchRef, primaryClickRef: primaryClickRef, onRuntimeError: reportRuntimeError, multiplayer: multiplayer, serverIdRef: serverIdRef })] }), _jsx(GameUiErrorBoundary, { onRuntimeError: reportRuntimeError, children: _jsx(GameProvider, { context: ctx, children: _jsx(GameUI, {}) }) }), showReticle ? _jsx(Reticle, {}) : null, _jsx(DiagnosticOverlay, { diagnostics: diagnostics })] }));
436
446
  }
@@ -2,7 +2,7 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  import { defineGame } from "@jgengine/core/game/defineGame";
3
3
  import { createAssetCatalog } from "@jgengine/core/scene/assetCatalog";
4
4
  import { CurrencyPill, HealthBar, SlotGrid } from "@jgengine/react/components";
5
- import { useFeed, useGameStore, usePlayer, useTarget } from "@jgengine/react/hooks";
5
+ import { useFeed, useGameClock, useGameStore, usePlayer, useTarget } from "@jgengine/react/hooks";
6
6
  const HERO = "hero";
7
7
  const MOB = "gloomSlime";
8
8
  const LOOT_TABLE = "slime-drops";
@@ -39,6 +39,7 @@ const game = defineGame({
39
39
  assets: createAssetCatalog(),
40
40
  multiplayer: null,
41
41
  inventories: { [HOTBAR]: { slots: 4, hud: "hotbar" } },
42
+ time: { start: 8 * 3600, speeds: [1, 2, 3, 4] },
42
43
  input: {
43
44
  moveForward: ["KeyW"],
44
45
  moveBack: ["KeyS"],
@@ -185,9 +186,16 @@ function KillFeedPanel() {
185
186
  function HotbarPanel() {
186
187
  return (_jsx(SlotGrid, { inventoryId: HOTBAR, className: "flex gap-1.5", renderSlot: (slot, index) => (_jsxs("div", { className: "w-16 rounded border border-white/20 bg-black/60 px-1.5 py-1 text-center text-[11px]", children: [_jsx("span", { className: "text-white/50", children: index + 1 }), _jsx("p", { className: "truncate text-white", children: slot === null ? "—" : slot.itemId })] })) }));
187
188
  }
189
+ function ClockPanel() {
190
+ const { paused, playSpeed, speeds, calendar, controls } = useGameClock();
191
+ const pad = (value) => value.toString().padStart(2, "0");
192
+ return (_jsxs("div", { className: "pointer-events-auto flex items-center gap-2 rounded bg-black/60 px-2 py-1 text-xs", children: [_jsxs("span", { className: "tabular-nums text-white", children: ["Day ", calendar.day + 1, " \u00B7 ", pad(calendar.hour), ":", pad(calendar.minute)] }), _jsx("button", { onClick: () => controls.toggle(), className: "rounded border border-white/25 px-1.5 py-0.5 text-white/80 hover:bg-white/10", children: paused ? "▶" : "⏸" }), _jsx("div", { className: "flex gap-0.5", children: speeds.map((speed) => (_jsxs("button", { onClick: () => controls.setSpeed(speed), className: `rounded border px-1.5 py-0.5 ${!paused && speed === playSpeed
193
+ ? "border-amber-300 bg-amber-300/20 text-amber-200"
194
+ : "border-white/25 text-white/70 hover:bg-white/10"}`, children: [speed, "\u00D7"] }, speed))) })] }));
195
+ }
188
196
  function DemoGameUI() {
189
197
  const player = usePlayer();
190
- return (_jsxs("div", { className: "pointer-events-none absolute inset-0 font-mono text-white", children: [_jsx("div", { className: "absolute left-1/2 top-4 w-56 -translate-x-1/2", children: _jsx(TargetPanel, { userId: player.userId }) }), _jsxs("div", { className: "absolute right-4 top-4 flex flex-col items-end gap-2", children: [_jsx(CurrencyPill, { currencyId: GOLD, className: "rounded bg-black/60 px-2 py-1 text-sm text-amber-300" }), _jsx(KillFeedPanel, {})] }), _jsx("div", { className: "absolute bottom-4 left-4 w-64", children: _jsx(VitalsPanel, { userId: player.userId }) }), _jsx("div", { className: "absolute bottom-4 left-1/2 -translate-x-1/2", children: _jsx(HotbarPanel, {}) }), _jsx("div", { className: "absolute bottom-4 right-4 max-w-[220px] text-right text-[11px] leading-4 text-white/40", children: "WASD move \u00B7 Space jump \u00B7 Shift sprint \u00B7 Q/E turn \u00B7 Tab target \u00B7 Esc clear \u00B7 1 sword \u00B7 2 zap" })] }));
198
+ return (_jsxs("div", { className: "pointer-events-none absolute inset-0 font-mono text-white", children: [_jsx("div", { className: "absolute left-4 top-4", children: _jsx(ClockPanel, {}) }), _jsx("div", { className: "absolute left-1/2 top-4 w-56 -translate-x-1/2", children: _jsx(TargetPanel, { userId: player.userId }) }), _jsxs("div", { className: "absolute right-4 top-4 flex flex-col items-end gap-2", children: [_jsx(CurrencyPill, { currencyId: GOLD, className: "rounded bg-black/60 px-2 py-1 text-sm text-amber-300" }), _jsx(KillFeedPanel, {})] }), _jsx("div", { className: "absolute bottom-4 left-4 w-64", children: _jsx(VitalsPanel, { userId: player.userId }) }), _jsx("div", { className: "absolute bottom-4 left-1/2 -translate-x-1/2", children: _jsx(HotbarPanel, {}) }), _jsx("div", { className: "absolute bottom-4 right-4 max-w-[220px] text-right text-[11px] leading-4 text-white/40", children: "WASD move \u00B7 Space jump \u00B7 Shift sprint \u00B7 Q/E turn \u00B7 Tab target \u00B7 Esc clear \u00B7 1 sword \u00B7 2 zap" })] }));
191
199
  }
192
200
  export const demoGame = {
193
201
  game,
@@ -0,0 +1,2 @@
1
+ import type { PlayableGame } from "../registry.js";
2
+ export declare const environmentShowcaseGame: PlayableGame;
@@ -0,0 +1,15 @@
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ import { building, environment, grass, ocean, rain, terrain, } from "@jgengine/core/world/features";
3
+ import { EnvironmentScene } from "../environment/EnvironmentScene.js";
4
+ import { demoGame } from "./demoGame.js";
5
+ const showcase = environment({
6
+ terrain: terrain({ bounds: { w: 220, d: 220 }, height: 4, frequency: 0.035, seed: "showcase", waterLevel: -1 }),
7
+ vegetation: grass({ area: { w: 120, d: 120 }, density: 5, colors: ["#31531f", "#8fbf4a"], seed: "showcase" }),
8
+ water: ocean({ bounds: { w: 520, d: 520 }, level: -1.4, waveHeight: 0.9, waveSpeed: 0.5, color: "#1f6f92" }),
9
+ weather: rain({ area: { w: 160, d: 160, h: 70 }, density: 0.5, wind: [1.4, 0.4] }),
10
+ structures: building({ count: 6, footprint: { w: 12, d: 9 }, stories: [3, 7], storyHeight: 2.8, spacing: 6, seed: "showcase" }),
11
+ });
12
+ export const environmentShowcaseGame = {
13
+ ...demoGame,
14
+ environment: () => _jsx(EnvironmentScene, { feature: showcase }),
15
+ };
@@ -0,0 +1,5 @@
1
+ import type { EnvironmentWorldFeature } from "@jgengine/core/world/features";
2
+ export interface EnvironmentSceneProps {
3
+ feature: EnvironmentWorldFeature;
4
+ }
5
+ export declare function EnvironmentScene({ feature }: EnvironmentSceneProps): import("react").JSX.Element;
@@ -0,0 +1,75 @@
1
+ import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { useMemo } from "react";
3
+ import { generateBuildingDistrict } from "@jgengine/core/world/buildings";
4
+ import { resolveTerrainField } from "@jgengine/core/world/terrain";
5
+ import { GeneratedBuilding } from "../structures/GeneratedBuilding.js";
6
+ import { GrassField } from "../terrain/GrassField.js";
7
+ import { ProceduralGround } from "../terrain/ProceduralGround.js";
8
+ import { Ocean } from "../water/Ocean.js";
9
+ import { RainField } from "../weather/RainField.js";
10
+ import { SnowField } from "../weather/SnowField.js";
11
+ import { WeatherUniformProvider } from "../weather/weatherUniforms.js";
12
+ const DEFAULT_TERRAIN_FREQUENCY = 0.03;
13
+ function TerrainGround({ terrain }) {
14
+ return (_jsx(ProceduralGround, { terrain: {
15
+ size: [terrain.bounds.w, terrain.bounds.d],
16
+ height: terrain.height,
17
+ seed: terrain.seed,
18
+ moundScale: terrain.frequency ?? DEFAULT_TERRAIN_FREQUENCY,
19
+ octaves: terrain.octaves,
20
+ ridged: terrain.ridged,
21
+ baseOffset: terrain.baseHeight,
22
+ }, colors: terrain.waterLevel === undefined ? undefined : { waterlineHeight: terrain.waterLevel } }));
23
+ }
24
+ function Vegetation({ grass, field }) {
25
+ return (_jsx(GrassField, { area: [grass.area.w, grass.area.d], density: grass.density, seed: grass.seed, bladeHeight: grass.bladeHeight, bladeWidth: grass.bladeWidth, heightAt: field.sampleHeight, colorBase: grass.colors[0], colorTip: grass.colors[grass.colors.length - 1], wind: { strength: grass.windStrength } }));
26
+ }
27
+ function weatherVolume(area) {
28
+ return [area.w, area.h ?? 60, area.d];
29
+ }
30
+ function Precipitation({ weather }) {
31
+ if (weather.kind === "rain") {
32
+ return (_jsx(RainField, { density: weather.density, speed: weather.speed, length: weather.dropLength, color: weather.color, wind: [weather.wind[0], 0, weather.wind[1]], volume: weatherVolume(weather.area) }));
33
+ }
34
+ return (_jsx(SnowField, { density: weather.density, speed: weather.speed, size: weather.flakeSize, sway: weather.drift, color: weather.color, wind: [weather.wind[0], 0, weather.wind[1]], volume: weatherVolume(weather.area) }));
35
+ }
36
+ function Weather({ weather }) {
37
+ return (_jsx(WeatherUniformProvider, { children: weather.map((entry, index) => (_jsx(Precipitation, { weather: entry, index: index }, `${entry.kind}-${index}`))) }));
38
+ }
39
+ function Water({ ocean }) {
40
+ return (_jsx(Ocean, { "position-y": ocean.level, config: {
41
+ size: Math.max(ocean.bounds.w, ocean.bounds.d),
42
+ amplitude: ocean.waveHeight,
43
+ speed: ocean.waveSpeed,
44
+ color: { shallow: ocean.color },
45
+ } }));
46
+ }
47
+ function Structures({ structures }) {
48
+ const buildings = useMemo(() => {
49
+ const columns = Math.max(1, Math.ceil(Math.sqrt(structures.count)));
50
+ const rows = Math.max(1, Math.ceil(structures.count / columns));
51
+ const spacing = structures.spacing;
52
+ const origin = [
53
+ -((columns - 1) * (structures.footprint.w + spacing)) / 2,
54
+ -((rows - 1) * (structures.footprint.d + spacing)) / 2,
55
+ ];
56
+ return generateBuildingDistrict({
57
+ rows,
58
+ columns,
59
+ origin,
60
+ lotSize: structures.footprint,
61
+ streetWidth: spacing,
62
+ floorRange: structures.stories,
63
+ base: { floorHeight: structures.storyHeight },
64
+ ...(structures.seed === undefined ? {} : { seed: structures.seed }),
65
+ }).slice(0, structures.count);
66
+ }, [structures]);
67
+ return (_jsx(_Fragment, { children: buildings.map((building) => (_jsx(GeneratedBuilding, { building: building }, building.id))) }));
68
+ }
69
+ export function EnvironmentScene({ feature }) {
70
+ const field = useMemo(() => resolveTerrainField(feature.terrain), [feature.terrain]);
71
+ const vegetation = feature.vegetation ?? [];
72
+ const water = feature.water ?? [];
73
+ const structures = feature.structures ?? [];
74
+ return (_jsxs(_Fragment, { children: [feature.terrain !== undefined ? _jsx(TerrainGround, { terrain: feature.terrain }) : null, water.map((ocean, index) => (_jsx(Water, { ocean: ocean }, `ocean-${index}`))), structures.map((entry, index) => (_jsx(Structures, { structures: entry }, `structures-${index}`))), vegetation.map((grass, index) => (_jsx(Vegetation, { grass: grass, field: field }, `grass-${index}`))), feature.weather !== undefined && feature.weather.length > 0 ? (_jsx(Weather, { weather: feature.weather })) : null] }));
75
+ }
@@ -0,0 +1 @@
1
+ export { EnvironmentScene, type EnvironmentSceneProps } from "./EnvironmentScene.js";
@@ -0,0 +1 @@
1
+ export { EnvironmentScene } from "./EnvironmentScene.js";
@@ -0,0 +1 @@
1
+ export * from "./environment/index.js";
@@ -0,0 +1 @@
1
+ export * from "./environment/index.js";
@@ -1,4 +1,6 @@
1
- import type { ComponentType } from "react";
1
+ import type { ComponentType, ReactNode } from "react";
2
2
  import type { PlayableGame as EnginePlayableGame } from "@jgengine/core/game/playableGame";
3
- export type PlayableGame = EnginePlayableGame<ComponentType, ComponentType>;
3
+ import type { SceneEntity } from "@jgengine/core/scene/entityStore";
4
+ export type RenderEntity = (entity: SceneEntity) => ReactNode;
5
+ export type PlayableGame = EnginePlayableGame<ComponentType, ComponentType, RenderEntity>;
4
6
  export type GameRegistry = Record<string, () => Promise<PlayableGame>>;
@@ -0,0 +1,44 @@
1
+ import { type ReactNode } from "react";
2
+ export type BuildingFacade = "front" | "back" | "left" | "right" | "roof";
3
+ export type BuildingPartKind = "wall" | "window" | "awning" | "airConditioner" | "clothesline" | "storefront" | "shutter" | "storeSign" | "roof" | "roofProp" | "guardrail" | "corner";
4
+ export interface BuildingPartPlacement {
5
+ id: string;
6
+ kind: BuildingPartKind;
7
+ facade: BuildingFacade;
8
+ position: readonly [number, number, number];
9
+ rotationY: number;
10
+ scale: readonly [number, number, number];
11
+ }
12
+ export interface GeneratedBuildingData {
13
+ id: string;
14
+ parts: readonly BuildingPartPlacement[];
15
+ }
16
+ export interface BuildingMaterialPalette {
17
+ wall?: string;
18
+ window?: string;
19
+ awning?: string;
20
+ airConditioner?: string;
21
+ clothesline?: string;
22
+ storefront?: string;
23
+ shutter?: string;
24
+ storeSign?: string;
25
+ roof?: string;
26
+ roofProp?: string;
27
+ guardrail?: string;
28
+ corner?: string;
29
+ }
30
+ export interface BuildingKitRenderer {
31
+ renderPart?: (part: BuildingPartPlacement) => ReactNode | undefined;
32
+ }
33
+ export interface BuildingBlockProps {
34
+ part: BuildingPartPlacement;
35
+ palette?: BuildingMaterialPalette;
36
+ }
37
+ export interface GeneratedBuildingProps {
38
+ building: GeneratedBuildingData;
39
+ palette?: BuildingMaterialPalette;
40
+ kit?: BuildingKitRenderer;
41
+ visibleKinds?: readonly BuildingPartKind[];
42
+ }
43
+ export declare function BuildingBlock({ part, palette }: BuildingBlockProps): import("react").JSX.Element;
44
+ export declare function GeneratedBuilding({ building, palette, kit, visibleKinds }: GeneratedBuildingProps): import("react").JSX.Element;
@@ -0,0 +1,86 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { useMemo } from "react";
3
+ const DEFAULT_PALETTE = {
4
+ wall: "#83766a",
5
+ window: "#8ecae6",
6
+ awning: "#c2410c",
7
+ airConditioner: "#d4d4d8",
8
+ clothesline: "#facc15",
9
+ storefront: "#3f3f46",
10
+ shutter: "#52525b",
11
+ storeSign: "#f97316",
12
+ roof: "#57534e",
13
+ roofProp: "#14b8a6",
14
+ guardrail: "#a8a29e",
15
+ corner: "#6b6258",
16
+ };
17
+ function colorFor(kind, palette) {
18
+ return palette?.[kind] ?? DEFAULT_PALETTE[kind];
19
+ }
20
+ function normalFor(facade) {
21
+ if (facade === "front")
22
+ return [0, 1];
23
+ if (facade === "back")
24
+ return [0, -1];
25
+ if (facade === "left")
26
+ return [-1, 0];
27
+ if (facade === "right")
28
+ return [1, 0];
29
+ return [0, 0];
30
+ }
31
+ function outwardOffset(part) {
32
+ if (part.kind === "wall" || part.kind === "roof" || part.kind === "corner")
33
+ return 0;
34
+ if (part.kind === "window" || part.kind === "shutter" || part.kind === "storefront")
35
+ return 0.025;
36
+ if (part.kind === "awning" || part.kind === "storeSign")
37
+ return 0.09;
38
+ return 0.12;
39
+ }
40
+ function materialFor(part, palette) {
41
+ const color = colorFor(part.kind, palette);
42
+ if (part.kind === "window" || part.kind === "storefront") {
43
+ return _jsx("meshPhysicalMaterial", { color: color, roughness: 0.12, metalness: 0, transparent: true, opacity: 0.56 });
44
+ }
45
+ if (part.kind === "storeSign") {
46
+ return _jsx("meshStandardMaterial", { color: color, emissive: color, emissiveIntensity: 0.6, roughness: 0.5 });
47
+ }
48
+ if (part.kind === "roofProp") {
49
+ return _jsx("meshStandardMaterial", { color: color, roughness: 0.65, metalness: 0.1 });
50
+ }
51
+ return _jsx("meshStandardMaterial", { color: color, roughness: 0.88, metalness: 0 });
52
+ }
53
+ function BlockMesh({ part, palette }) {
54
+ const [nx, nz] = normalFor(part.facade);
55
+ const offset = outwardOffset(part);
56
+ const position = [
57
+ part.position[0] + nx * offset,
58
+ part.position[1],
59
+ part.position[2] + nz * offset,
60
+ ];
61
+ return (_jsxs("mesh", { position: position, rotation: [0, part.rotationY, 0], castShadow: true, receiveShadow: true, children: [_jsx("boxGeometry", { args: [part.scale[0], part.scale[1], part.scale[2]] }), materialFor(part, palette)] }));
62
+ }
63
+ export function BuildingBlock({ part, palette }) {
64
+ if (part.kind === "clothesline") {
65
+ const [nx, nz] = normalFor(part.facade);
66
+ const offset = outwardOffset(part);
67
+ const position = [
68
+ part.position[0] + nx * offset,
69
+ part.position[1],
70
+ part.position[2] + nz * offset,
71
+ ];
72
+ return (_jsxs("group", { position: position, rotation: [0, part.rotationY, 0], children: [_jsxs("mesh", { position: [0, 0, -0.09], castShadow: true, children: [_jsx("boxGeometry", { args: [part.scale[0], Math.max(part.scale[1], 0.025), 0.025] }), materialFor(part, palette)] }), _jsxs("mesh", { position: [0, 0, 0.09], castShadow: true, children: [_jsx("boxGeometry", { args: [part.scale[0], Math.max(part.scale[1], 0.025), 0.025] }), materialFor(part, palette)] })] }));
73
+ }
74
+ return _jsx(BlockMesh, { part: part, palette: palette });
75
+ }
76
+ export function GeneratedBuilding({ building, palette, kit, visibleKinds }) {
77
+ const visible = useMemo(() => (visibleKinds === undefined ? null : new Set(visibleKinds)), [visibleKinds]);
78
+ return (_jsx("group", { name: building.id, children: building.parts.map((part) => {
79
+ if (visible !== null && !visible.has(part.kind))
80
+ return null;
81
+ const rendered = kit?.renderPart?.(part);
82
+ if (rendered !== undefined)
83
+ return _jsx("group", { children: rendered }, part.id);
84
+ return _jsx(BuildingBlock, { part: part, palette: palette }, part.id);
85
+ }) }));
86
+ }
@@ -0,0 +1,21 @@
1
+ import { type ThreeElements } from "@react-three/fiber";
2
+ import { type GrassBladeGeometryOptions, type GrassRange } from "./grassGeometry.js";
3
+ import { type GrassMaterialOptions, type GrassWindOptions } from "./grassMaterial.js";
4
+ import type { TerrainArea, TerrainHeightSampler } from "./terrainMath.js";
5
+ export interface GrassFieldProps extends Omit<ThreeElements["mesh"], "args" | "children" | "geometry" | "material"> {
6
+ count?: number;
7
+ density?: number;
8
+ area?: TerrainArea;
9
+ seed?: GrassBladeGeometryOptions["seed"];
10
+ segments?: number;
11
+ bladeHeight?: GrassRange;
12
+ bladeWidth?: GrassRange;
13
+ bladeBend?: GrassRange;
14
+ heightAt?: TerrainHeightSampler;
15
+ colorBase?: GrassMaterialOptions["colorBase"];
16
+ colorTip?: GrassMaterialOptions["colorTip"];
17
+ colorVariation?: number;
18
+ wind?: GrassWindOptions | false;
19
+ roughness?: number;
20
+ }
21
+ export declare function GrassField({ count, density, area, seed, segments, bladeHeight, bladeWidth, bladeBend, heightAt, colorBase, colorTip, colorVariation, wind, roughness, castShadow, receiveShadow, frustumCulled, ...meshProps }: GrassFieldProps): import("react").JSX.Element;
@@ -0,0 +1,31 @@
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ import { useFrame } from "@react-three/fiber";
3
+ import { useEffect, useMemo } from "react";
4
+ import { createGrassBladeGeometry } from "./grassGeometry.js";
5
+ import { createGrassMaterial } from "./grassMaterial.js";
6
+ export function GrassField({ count = 6000, density = 1, area = 40, seed = 1, segments = 4, bladeHeight, bladeWidth, bladeBend, heightAt, colorBase, colorTip, colorVariation, wind, roughness, castShadow = false, receiveShadow = true, frustumCulled = false, ...meshProps }) {
7
+ const geometry = useMemo(() => createGrassBladeGeometry({
8
+ count,
9
+ area,
10
+ seed,
11
+ segments,
12
+ height: bladeHeight,
13
+ width: bladeWidth,
14
+ bend: bladeBend,
15
+ heightAt,
16
+ }), [area, bladeBend, bladeHeight, bladeWidth, count, heightAt, seed, segments]);
17
+ const handle = useMemo(() => createGrassMaterial({
18
+ colorBase,
19
+ colorTip,
20
+ colorVariation,
21
+ wind,
22
+ roughness,
23
+ }), [colorBase, colorTip, colorVariation, roughness, wind]);
24
+ geometry.instanceCount = Math.floor(Math.max(0, Math.min(1, density)) * count);
25
+ useFrame((state) => {
26
+ handle.uniforms.uTime.value = state.clock.elapsedTime;
27
+ });
28
+ useEffect(() => () => geometry.dispose(), [geometry]);
29
+ useEffect(() => () => handle.material.dispose(), [handle]);
30
+ return (_jsx("mesh", { ...meshProps, geometry: geometry, material: handle.material, castShadow: castShadow, receiveShadow: receiveShadow, frustumCulled: frustumCulled }));
31
+ }
@@ -0,0 +1,9 @@
1
+ import { type ThreeElements } from "@react-three/fiber";
2
+ import { type ProceduralTerrainConfig, type TerrainVertexColorOptions } from "./terrainMath.js";
3
+ export interface ProceduralGroundProps extends Omit<ThreeElements["mesh"], "args" | "children" | "geometry" | "material"> {
4
+ terrain?: ProceduralTerrainConfig;
5
+ colors?: TerrainVertexColorOptions;
6
+ roughness?: number;
7
+ metalness?: number;
8
+ }
9
+ export declare function ProceduralGround({ terrain, colors, roughness, metalness, receiveShadow, ...meshProps }: ProceduralGroundProps): import("react").JSX.Element;
@@ -0,0 +1,17 @@
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ import {} from "@react-three/fiber";
3
+ import { useEffect, useMemo } from "react";
4
+ import * as THREE from "three";
5
+ import { createProceduralGroundGeometry } from "./terrainMath.js";
6
+ export function ProceduralGround({ terrain, colors, roughness = 0.94, metalness = 0, receiveShadow = true, ...meshProps }) {
7
+ const geometry = useMemo(() => createProceduralGroundGeometry(terrain, colors), [colors, terrain]);
8
+ const material = useMemo(() => new THREE.MeshStandardMaterial({
9
+ color: "#ffffff",
10
+ roughness,
11
+ metalness,
12
+ vertexColors: true,
13
+ }), [metalness, roughness]);
14
+ useEffect(() => () => geometry.dispose(), [geometry]);
15
+ useEffect(() => () => material.dispose(), [material]);
16
+ return _jsx("mesh", { ...meshProps, geometry: geometry, material: material, receiveShadow: receiveShadow });
17
+ }
@@ -0,0 +1,27 @@
1
+ import * as THREE from "three";
2
+ import { type TerrainSeed } from "./random.js";
3
+ import { type TerrainArea, type TerrainHeightSampler } from "./terrainMath.js";
4
+ export type GrassRange = number | readonly [min: number, max: number];
5
+ export interface GrassBladeGeometryOptions {
6
+ count?: number;
7
+ area?: TerrainArea;
8
+ seed?: TerrainSeed;
9
+ segments?: number;
10
+ height?: GrassRange;
11
+ width?: GrassRange;
12
+ bend?: GrassRange;
13
+ heightAt?: TerrainHeightSampler;
14
+ }
15
+ export interface ResolvedGrassBladeGeometryOptions {
16
+ count: number;
17
+ area: TerrainArea;
18
+ seed: TerrainSeed;
19
+ segments: number;
20
+ height: readonly [min: number, max: number];
21
+ width: readonly [min: number, max: number];
22
+ bend: readonly [min: number, max: number];
23
+ heightAt: TerrainHeightSampler;
24
+ }
25
+ export declare function resolveGrassRange(value: GrassRange | undefined, fallback: readonly [number, number]): readonly [number, number];
26
+ export declare function resolveGrassBladeGeometryOptions(options?: GrassBladeGeometryOptions): ResolvedGrassBladeGeometryOptions;
27
+ export declare function createGrassBladeGeometry(options?: GrassBladeGeometryOptions): THREE.InstancedBufferGeometry;