@jgengine/shell 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.
- package/CHANGELOG.md +66 -0
- package/README.md +27 -0
- package/dist/GamePlayerShell.js +19 -9
- package/dist/demo/demoGame.js +10 -2
- package/dist/demo/environmentShowcase.d.ts +2 -0
- package/dist/demo/environmentShowcase.js +15 -0
- package/dist/environment/EnvironmentScene.d.ts +5 -0
- package/dist/environment/EnvironmentScene.js +75 -0
- package/dist/environment/index.d.ts +1 -0
- package/dist/environment/index.js +1 -0
- package/dist/environment.d.ts +1 -0
- package/dist/environment.js +1 -0
- package/dist/registry.d.ts +4 -2
- package/dist/structures/GeneratedBuilding.d.ts +44 -0
- package/dist/structures/GeneratedBuilding.js +86 -0
- package/dist/terrain/GrassField.d.ts +21 -0
- package/dist/terrain/GrassField.js +31 -0
- package/dist/terrain/ProceduralGround.d.ts +9 -0
- package/dist/terrain/ProceduralGround.js +17 -0
- package/dist/terrain/grassGeometry.d.ts +27 -0
- package/dist/terrain/grassGeometry.js +77 -0
- package/dist/terrain/grassMaterial.d.ts +33 -0
- package/dist/terrain/grassMaterial.js +108 -0
- package/dist/terrain/index.d.ts +7 -0
- package/dist/terrain/index.js +7 -0
- package/dist/terrain/random.d.ts +4 -0
- package/dist/terrain/random.js +27 -0
- package/dist/terrain/terrainMath.d.ts +34 -0
- package/dist/terrain/terrainMath.js +92 -0
- package/dist/terrain.d.ts +1 -0
- package/dist/terrain.js +1 -0
- package/dist/water/Ocean.d.ts +6 -0
- package/dist/water/Ocean.js +30 -0
- package/dist/water/OceanConfig.d.ts +90 -0
- package/dist/water/OceanConfig.js +131 -0
- package/dist/water/OceanMaterial.d.ts +54 -0
- package/dist/water/OceanMaterial.js +51 -0
- package/dist/water/OceanShader.d.ts +2 -0
- package/dist/water/OceanShader.js +78 -0
- package/dist/water/index.d.ts +3 -0
- package/dist/water/index.js +3 -0
- package/dist/water.d.ts +1 -0
- package/dist/water.js +1 -0
- package/dist/weather/LightningStrike.d.ts +17 -0
- package/dist/weather/LightningStrike.js +130 -0
- package/dist/weather/RainField.d.ts +20 -0
- package/dist/weather/RainField.js +107 -0
- package/dist/weather/SnowField.d.ts +19 -0
- package/dist/weather/SnowField.js +108 -0
- package/dist/weather/WeatherLayer.d.ts +17 -0
- package/dist/weather/WeatherLayer.js +16 -0
- package/dist/weather/index.d.ts +5 -0
- package/dist/weather/index.js +5 -0
- package/dist/weather/weatherGeometry.d.ts +2 -0
- package/dist/weather/weatherGeometry.js +14 -0
- package/dist/weather/weatherMath.d.ts +7 -0
- package/dist/weather/weatherMath.js +30 -0
- package/dist/weather/weatherUniforms.d.ts +18 -0
- package/dist/weather/weatherUniforms.js +34 -0
- package/dist/world/InstancedBodies.d.ts +17 -0
- package/dist/world/InstancedBodies.js +84 -0
- package/package.json +12 -5
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,66 @@
|
|
|
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.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
|
+
|
|
42
|
+
## 0.5.0
|
|
43
|
+
|
|
44
|
+
An additive release: **every 0.4.0 API is unchanged**, so upgrading is only a
|
|
45
|
+
version bump. New pure primitives across progression, inventory slots, world
|
|
46
|
+
geometry, and React store bindings.
|
|
47
|
+
|
|
48
|
+
### Migrate
|
|
49
|
+
|
|
50
|
+
- Bump every `@jgengine/*` dependency to `^0.5.0` (the eight packages version in lockstep).
|
|
51
|
+
- No code change is required — 0.5.0 adds surface, it doesn't move or remove any.
|
|
52
|
+
- 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.
|
|
53
|
+
|
|
54
|
+
### Added
|
|
55
|
+
|
|
56
|
+
- `@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.
|
|
57
|
+
- `@jgengine/core/inventory/slotModel` — pure slot-grid primitives (`createSlots`, `placeAt`, `removeAt`, `moveSlot`).
|
|
58
|
+
- `@jgengine/core/world/geometry`, `/world/interiors`, `/world/placement` — pure world primitives: grid snapping, footprint AABBs and overlap, interior/exterior spaces, and placement validation.
|
|
59
|
+
- `@jgengine/react/engineStore` — raw-store React bindings (`useEngineState`, `useEngineStore`, `useEngineEvent`).
|
|
60
|
+
- Pure/functional tiers for the `trade`, `unlocks`, `quest`, and `feed` verbs in `@jgengine/core/game`.
|
|
61
|
+
|
|
62
|
+
## 0.4.0
|
|
63
|
+
|
|
64
|
+
Baseline release: the eight `@jgengine/*` packages (core, ws, sql, react,
|
|
65
|
+
convex, node, shell, assets) as the first tracked version. No migration —
|
|
66
|
+
this is the floor changelog entries are measured against.
|
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
|
+
```
|
package/dist/GamePlayerShell.js
CHANGED
|
@@ -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
|
-
|
|
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
|
|
96
|
-
|
|
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,
|
|
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
|
}
|
package/dist/demo/demoGame.js
CHANGED
|
@@ -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,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,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";
|
package/dist/registry.d.ts
CHANGED
|
@@ -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
|
-
|
|
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;
|