@aethervtt/shard-scatter 0.0.0-stage → 0.2.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/LICENSE +21 -0
- package/README.md +2 -2
- package/dist/components.d.ts +44 -0
- package/dist/components.js +98 -0
- package/dist/foliage.d.ts +43 -0
- package/dist/foliage.js +175 -0
- package/dist/heightfield-surface.d.ts +35 -0
- package/dist/heightfield-surface.js +61 -0
- package/dist/index.d.ts +13 -0
- package/dist/index.js +120 -0
- package/dist/material.d.ts +20 -0
- package/dist/material.js +149 -0
- package/dist/mesh-surface.d.ts +79 -0
- package/dist/mesh-surface.js +470 -0
- package/dist/methods.d.ts +10 -0
- package/dist/methods.js +341 -0
- package/dist/overlays.d.ts +4 -0
- package/dist/overlays.js +84 -0
- package/dist/planet.d.ts +74 -0
- package/dist/planet.js +675 -0
- package/dist/plugin.d.ts +17 -0
- package/dist/plugin.js +265 -0
- package/dist/preview-render.d.ts +10 -0
- package/dist/preview-render.js +155 -0
- package/dist/preview.d.ts +5 -0
- package/dist/preview.js +11 -0
- package/dist/rules.d.ts +97 -0
- package/dist/rules.js +170 -0
- package/dist/runtime.d.ts +138 -0
- package/dist/runtime.js +702 -0
- package/dist/set.d.ts +45 -0
- package/dist/set.js +131 -0
- package/dist/surface.d.ts +124 -0
- package/dist/surface.js +20 -0
- package/dist/testing.d.ts +236 -0
- package/dist/testing.js +231 -0
- package/dist/viewers.d.ts +9 -0
- package/dist/viewers.js +22 -0
- package/package.json +49 -4
package/dist/rules.js
ADDED
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
import { hash32, hashSeed, ShardError } from "@aethervtt/shard-core";
|
|
2
|
+
const PROP = 0;
|
|
3
|
+
const FOLIAGE = 1;
|
|
4
|
+
const MAX_PROPS_PER_CHUNK = 1024;
|
|
5
|
+
const MAX_FOLIAGE_PER_CHUNK = 16384;
|
|
6
|
+
const MIN_JITTER = 0.4;
|
|
7
|
+
function compileRules(sources, surfaceSeed) {
|
|
8
|
+
const out = [];
|
|
9
|
+
for (const source of sources) {
|
|
10
|
+
const byName = /* @__PURE__ */ new Map();
|
|
11
|
+
for (const [r, rule] of source.set.rules.entries()) {
|
|
12
|
+
const path = `${source.path}#/rules/${r}`;
|
|
13
|
+
if (byName.has(rule.name)) {
|
|
14
|
+
throw new ShardError("scatter/duplicate-rule", `Two rules are named "${rule.name}"`, {
|
|
15
|
+
path,
|
|
16
|
+
hint: "Rule names are unique within a set: saves and `avoid` refer to them."
|
|
17
|
+
});
|
|
18
|
+
}
|
|
19
|
+
const avoid = [];
|
|
20
|
+
for (const name of rule.avoid) {
|
|
21
|
+
const target = byName.get(name);
|
|
22
|
+
if (target === void 0) {
|
|
23
|
+
throw new ShardError(
|
|
24
|
+
"scatter/unknown-rule",
|
|
25
|
+
`Rule "${rule.name}" avoids "${name}", which isn't an earlier rule of its set`,
|
|
26
|
+
{
|
|
27
|
+
path: `${path}/avoid`,
|
|
28
|
+
hint: `Earlier rules: ${[...byName.keys()].join(", ") || "none"}. Rule order is priority, so the avoided rule goes first.`
|
|
29
|
+
}
|
|
30
|
+
);
|
|
31
|
+
}
|
|
32
|
+
avoid.push(target);
|
|
33
|
+
}
|
|
34
|
+
const items = rule.items.filter((i) => i.weight > 0);
|
|
35
|
+
if (items.length === 0) {
|
|
36
|
+
throw new ShardError("scatter/no-items", `Rule "${rule.name}" has nothing to place`, {
|
|
37
|
+
path: `${path}/items`,
|
|
38
|
+
hint: 'Add an item: { "generator": "shard/Rock" } or { "prefab": { "path": "\u2026" } }.'
|
|
39
|
+
});
|
|
40
|
+
}
|
|
41
|
+
for (const [i, item] of items.entries()) {
|
|
42
|
+
if (!item.prefab && !item.generator) {
|
|
43
|
+
throw new ShardError(
|
|
44
|
+
"scatter/item-not-mesh",
|
|
45
|
+
`Item ${i} of rule "${rule.name}" has neither a prefab nor a generator`,
|
|
46
|
+
{ path: `${path}/items/${i}`, hint: 'Set "generator" (a mesh generator) or "prefab".' }
|
|
47
|
+
);
|
|
48
|
+
}
|
|
49
|
+
if (rule.kind === "foliage" && !item.generator) {
|
|
50
|
+
throw new ShardError(
|
|
51
|
+
"scatter/item-not-mesh",
|
|
52
|
+
`Foliage rule "${rule.name}" places a prefab; foliage draws meshes only`,
|
|
53
|
+
{
|
|
54
|
+
path: `${path}/items/${i}`,
|
|
55
|
+
hint: 'Use a mesh generator item ("shard/GrassClump"), or make the rule kind "prop".'
|
|
56
|
+
}
|
|
57
|
+
);
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
const kind = rule.kind === "foliage" ? FOLIAGE : PROP;
|
|
61
|
+
const cell = cellSize(rule.density, rule.spacing);
|
|
62
|
+
if (rule.density > 0 && rule.density * cell * cell > 1 + 1e-9) {
|
|
63
|
+
const most = (1 - MIN_JITTER) ** 2 / (rule.spacing * rule.spacing);
|
|
64
|
+
throw new ShardError(
|
|
65
|
+
"scatter/density-too-high",
|
|
66
|
+
`Rule "${rule.name}" asks for ${rule.density}/m\xB2 with ${rule.spacing} m spacing; at most ${most.toPrecision(3)}/m\xB2 fit`,
|
|
67
|
+
{
|
|
68
|
+
path: `${path}/density`,
|
|
69
|
+
hint: "Lower the density or the spacing: items keep `spacing` apart on a jittered lattice."
|
|
70
|
+
}
|
|
71
|
+
);
|
|
72
|
+
}
|
|
73
|
+
let sum = 0;
|
|
74
|
+
for (const item of items) sum += item.weight;
|
|
75
|
+
const weights = new Float64Array(items.length);
|
|
76
|
+
let acc = 0;
|
|
77
|
+
for (const [i, item] of items.entries()) {
|
|
78
|
+
acc += item.weight / sum;
|
|
79
|
+
weights[i] = i === items.length - 1 ? 1 : acc;
|
|
80
|
+
}
|
|
81
|
+
const id = `${source.path}${source.biome >= 0 ? `@${source.biome}` : ""}:${rule.name}`;
|
|
82
|
+
out.push({
|
|
83
|
+
index: out.length,
|
|
84
|
+
name: rule.name,
|
|
85
|
+
id,
|
|
86
|
+
kind,
|
|
87
|
+
biome: source.biome,
|
|
88
|
+
seed: hashSeed(surfaceSeed ^ source.set.seed, id),
|
|
89
|
+
density: rule.density,
|
|
90
|
+
spacing: rule.spacing,
|
|
91
|
+
cell,
|
|
92
|
+
slopeLo: rule.masks.slope[0],
|
|
93
|
+
slopeHi: rule.masks.slope[1],
|
|
94
|
+
heightLo: rule.masks.height[0],
|
|
95
|
+
heightHi: rule.masks.height[1],
|
|
96
|
+
noise: rule.masks.noise.graph,
|
|
97
|
+
noiseAbove: rule.masks.noise.above,
|
|
98
|
+
align: rule.align,
|
|
99
|
+
scaleMin: Math.min(rule.scale[0], rule.scale[1]),
|
|
100
|
+
scaleMax: Math.max(rule.scale[0], rule.scale[1]),
|
|
101
|
+
sink: rule.sink,
|
|
102
|
+
collider: rule.collider,
|
|
103
|
+
range: rule.range,
|
|
104
|
+
wind: rule.wind,
|
|
105
|
+
shadowRange: rule.shadowRange,
|
|
106
|
+
avoid: avoid.map((a) => out.length - byName.size + a),
|
|
107
|
+
items: items.map((i) => ({
|
|
108
|
+
prefab: i.prefab,
|
|
109
|
+
generator: i.generator,
|
|
110
|
+
params: i.params && typeof i.params === "object" && !Array.isArray(i.params) ? i.params : {},
|
|
111
|
+
variants: Math.max(1, i.variants),
|
|
112
|
+
material: i.material,
|
|
113
|
+
radius: i.radius
|
|
114
|
+
})),
|
|
115
|
+
weights
|
|
116
|
+
});
|
|
117
|
+
byName.set(rule.name, r);
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
return out;
|
|
121
|
+
}
|
|
122
|
+
function cellSize(density, spacing) {
|
|
123
|
+
const byDensity = density > 0 ? 1 / Math.sqrt(2 * density) : Number.POSITIVE_INFINITY;
|
|
124
|
+
const bySpacing = spacing > 0 ? spacing / (1 - MIN_JITTER) : 0;
|
|
125
|
+
return Math.max(byDensity, bySpacing);
|
|
126
|
+
}
|
|
127
|
+
function jitterFor(cell, spacing) {
|
|
128
|
+
if (!(spacing > 0)) return 1;
|
|
129
|
+
return Math.max(0, Math.min(1, 1 - spacing / cell));
|
|
130
|
+
}
|
|
131
|
+
const STREAM_ACCEPT = 0;
|
|
132
|
+
const STREAM_X = 1;
|
|
133
|
+
const STREAM_Y = 2;
|
|
134
|
+
const STREAM_ITEM = 3;
|
|
135
|
+
const STREAM_VARIANT = 4;
|
|
136
|
+
const STREAM_YAW = 5;
|
|
137
|
+
const STREAM_SCALE = 6;
|
|
138
|
+
const STREAM_SHADE = 7;
|
|
139
|
+
const INV32 = 1 / 4294967296;
|
|
140
|
+
function cellRandom(seed, gi, gj, domain, stream) {
|
|
141
|
+
return hash32(seed, gi, gj, domain, stream) * INV32;
|
|
142
|
+
}
|
|
143
|
+
function pickItem(rule, u) {
|
|
144
|
+
const w = rule.weights;
|
|
145
|
+
for (let i = 0; i < w.length - 1; i++) if (u < w[i]) return i;
|
|
146
|
+
return w.length - 1;
|
|
147
|
+
}
|
|
148
|
+
function inRange(x, lo, hi) {
|
|
149
|
+
return !(lo < hi) || x >= lo && x <= hi;
|
|
150
|
+
}
|
|
151
|
+
export {
|
|
152
|
+
FOLIAGE,
|
|
153
|
+
MAX_FOLIAGE_PER_CHUNK,
|
|
154
|
+
MAX_PROPS_PER_CHUNK,
|
|
155
|
+
PROP,
|
|
156
|
+
STREAM_ACCEPT,
|
|
157
|
+
STREAM_ITEM,
|
|
158
|
+
STREAM_SCALE,
|
|
159
|
+
STREAM_SHADE,
|
|
160
|
+
STREAM_VARIANT,
|
|
161
|
+
STREAM_X,
|
|
162
|
+
STREAM_Y,
|
|
163
|
+
STREAM_YAW,
|
|
164
|
+
cellRandom,
|
|
165
|
+
cellSize,
|
|
166
|
+
compileRules,
|
|
167
|
+
inRange,
|
|
168
|
+
jitterFor,
|
|
169
|
+
pickItem
|
|
170
|
+
};
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
import { type AssetRef, type Entity, ShardError, type World } from '@aethervtt/shard-core';
|
|
2
|
+
import type { Workers } from '@aethervtt/shard-platform';
|
|
3
|
+
import { type GenRequest } from '@aethervtt/shard-procgen';
|
|
4
|
+
import { type PlanetRuntime } from '@aethervtt/shard-terrain';
|
|
5
|
+
import { type ScatterBudgetValue } from './components.js';
|
|
6
|
+
import type { FoliageRule } from './foliage.js';
|
|
7
|
+
import { type CompiledRule, type SetSource } from './rules.js';
|
|
8
|
+
import { type ChunkPlacements, type PlacementJob, type Surface, type SurfaceChunk } from './surface.js';
|
|
9
|
+
/** Frames between wanting a chunk's placements and having them, pool or not (deterministic). */
|
|
10
|
+
export declare const PLACEMENT_DELAY = 2;
|
|
11
|
+
/** One mesh an item places: a generator variant (with LODs) or a prefab. */
|
|
12
|
+
export interface ItemVariant {
|
|
13
|
+
request: GenRequest | undefined;
|
|
14
|
+
mesh: AssetRef<'Mesh'> | null;
|
|
15
|
+
lods: AssetRef<'Mesh'>[];
|
|
16
|
+
prefab: AssetRef<'Prefab'> | null;
|
|
17
|
+
material: AssetRef<'Material'> | null;
|
|
18
|
+
/** Bounds height and lowest point (m, before scale). */
|
|
19
|
+
height: number;
|
|
20
|
+
minY: number;
|
|
21
|
+
/** Footprint radius before scale (m). */
|
|
22
|
+
radius: number;
|
|
23
|
+
/** Half extents and center of the bounds (cuboid colliders). */
|
|
24
|
+
half: [number, number, number];
|
|
25
|
+
mid: [number, number, number];
|
|
26
|
+
ready: boolean;
|
|
27
|
+
/** The components every prop of this variant spawns with (made on first spawn). */
|
|
28
|
+
parts?: unknown[];
|
|
29
|
+
}
|
|
30
|
+
/** One rule's chunk: its placements, its job while placing, and its entity while spawned. */
|
|
31
|
+
export interface ChunkState {
|
|
32
|
+
id: string;
|
|
33
|
+
chunk: SurfaceChunk;
|
|
34
|
+
/** Placements before `avoid`. */
|
|
35
|
+
raw: ChunkPlacements | undefined;
|
|
36
|
+
/** Placements with `avoid` applied: what spawns. */
|
|
37
|
+
placements: ChunkPlacements | undefined;
|
|
38
|
+
job: PlacementJob | undefined;
|
|
39
|
+
root: Entity;
|
|
40
|
+
/** Frame a viewer last had it within range (spawn) and within KEEP × range (keep). */
|
|
41
|
+
wanted: number;
|
|
42
|
+
kept: number;
|
|
43
|
+
distance: number;
|
|
44
|
+
/** Frame last touched (cache order). */
|
|
45
|
+
used: number;
|
|
46
|
+
spawned: number;
|
|
47
|
+
/** The next placement to spawn: a chunk spawns over several frames within the budget. */
|
|
48
|
+
next: number;
|
|
49
|
+
/** The avoided rules' chunks around it (placed before it spawns). */
|
|
50
|
+
deps?: ChunkState[];
|
|
51
|
+
/** Frame another chunk last needed its placements (for `avoid`). */
|
|
52
|
+
needed?: number;
|
|
53
|
+
}
|
|
54
|
+
/** A chunk's state before anything is placed or spawned. */
|
|
55
|
+
export declare function chunkState(id: string, chunk: SurfaceChunk, frame: number, distance?: number): ChunkState;
|
|
56
|
+
export interface RuleStats {
|
|
57
|
+
chunks: number;
|
|
58
|
+
placed: number;
|
|
59
|
+
spawned: number;
|
|
60
|
+
candidates: number;
|
|
61
|
+
}
|
|
62
|
+
/** A surface's scatter state: compiled rules, item meshes, chunks, jobs. */
|
|
63
|
+
export declare class SurfaceScatter {
|
|
64
|
+
readonly surface: Surface;
|
|
65
|
+
/** What the compiled rules came from (sets as JSON, seeds, planet contents). */
|
|
66
|
+
key: string;
|
|
67
|
+
/** The surface's content version last refreshed against. */
|
|
68
|
+
content: string;
|
|
69
|
+
problem: ShardError | null;
|
|
70
|
+
waiting: string | null;
|
|
71
|
+
/** [rule][item][variant] */
|
|
72
|
+
variants: ItemVariant[][][];
|
|
73
|
+
readonly chunks: Map<string, ChunkState>;
|
|
74
|
+
/** Chunks spawned or waiting to, by id. */
|
|
75
|
+
ready: boolean;
|
|
76
|
+
readonly stats: RuleStats[];
|
|
77
|
+
/** Foliage rules' GPU layers (with a renderer), by rule index, and the rules' version they're for. */
|
|
78
|
+
readonly foliage: Map<number, FoliageRule>;
|
|
79
|
+
foliageVersion: number;
|
|
80
|
+
/** Props spawned and despawned last frame, main-thread ms of the scatter system. */
|
|
81
|
+
spawnedLast: number;
|
|
82
|
+
despawnedLast: number;
|
|
83
|
+
ms: number;
|
|
84
|
+
/** Main-thread ms spawning and despawning props last frame. */
|
|
85
|
+
spawnMs: number;
|
|
86
|
+
constructor(surface: Surface);
|
|
87
|
+
}
|
|
88
|
+
/** Every surface's scatter state and the frame counter. */
|
|
89
|
+
export declare class ScatterState {
|
|
90
|
+
readonly surfaces: Map<number, SurfaceScatter>;
|
|
91
|
+
frame: number;
|
|
92
|
+
workers: Workers | undefined;
|
|
93
|
+
/** Set when an asset (re)loaded: surfaces re-resolve their sets. */
|
|
94
|
+
dirty: boolean;
|
|
95
|
+
/** True while scatter despawns its own chunks (so props aren't recorded as removed). */
|
|
96
|
+
despawning: boolean;
|
|
97
|
+
/** Default vegetation materials, per generator name. */
|
|
98
|
+
readonly materials: Map<string, AssetRef<"Material">>;
|
|
99
|
+
/** Props despawned outside scatter since the last frame (checked, then recorded as removed). */
|
|
100
|
+
readonly pendingRemovals: {
|
|
101
|
+
rule: string;
|
|
102
|
+
chunk: string;
|
|
103
|
+
index: number;
|
|
104
|
+
root: Entity;
|
|
105
|
+
}[];
|
|
106
|
+
}
|
|
107
|
+
export declare const Scatter: import("@aethervtt/shard-core").ResourceDef<ScatterState>;
|
|
108
|
+
/** A loaded asset, undefined for no handle, or null while it loads (the load starts here). */
|
|
109
|
+
export declare function resolveAsset<T>(world: World, ref: AssetRef | null | undefined, get: (guid: string) => T | undefined): T | undefined | null;
|
|
110
|
+
/** The sets a planet scatters: its own, then each biome's (where that biome dominates). */
|
|
111
|
+
export declare function planetSources(world: World, rt: PlanetRuntime): {
|
|
112
|
+
sources: SetSource[];
|
|
113
|
+
waiting: string | null;
|
|
114
|
+
};
|
|
115
|
+
/** Whether a planet scatters anything (its own set, or any biome's). */
|
|
116
|
+
export declare function planetScatters(world: World, rt: PlanetRuntime): boolean;
|
|
117
|
+
/**
|
|
118
|
+
* Brings a surface's compiled rules and item meshes up to date. Cheap when nothing changed (a key
|
|
119
|
+
* compare). Returns whether it can place.
|
|
120
|
+
*/
|
|
121
|
+
export declare function refreshSurface(world: World, ss: SurfaceScatter, sources: SetSource[], seed: number, contentKey: string): boolean;
|
|
122
|
+
/** The engine's look for a generator's items (scatter/Vegetation), made once per world. */
|
|
123
|
+
export declare function defaultMaterial(world: World, generator: string): AssetRef<'Material'> | null;
|
|
124
|
+
/** Despawns every chunk and forgets placements (rules or the surface changed). */
|
|
125
|
+
export declare function clearChunks(world: World, ss: SurfaceScatter): void;
|
|
126
|
+
/**
|
|
127
|
+
* One frame of a surface's props (spec 0045): which chunks viewers want, placement (on the pool,
|
|
128
|
+
* finished inline when due), `avoid`, then spawning nearest first within the budget and despawning
|
|
129
|
+
* what's out of range. Deterministic: the same viewers give the same entities on every host.
|
|
130
|
+
*/
|
|
131
|
+
export declare function updateProps(world: World, ss: SurfaceScatter, frame: number, budget: ScatterBudgetValue, workers: Workers | undefined): void;
|
|
132
|
+
/**
|
|
133
|
+
* A chunk's placements with `avoid` applied: anything within an avoided item's footprint is
|
|
134
|
+
* dropped. The avoided rules' chunks around it are placed first (inline if they aren't yet).
|
|
135
|
+
*/
|
|
136
|
+
export declare function finalPlacements(ss: SurfaceScatter, c: ChunkState, frame: number): ChunkPlacements;
|
|
137
|
+
/** The avoided rule's chunks that could hold an item reaching into `chunk`. */
|
|
138
|
+
export declare function neighborChunks(ss: SurfaceScatter, avoided: CompiledRule, chunk: SurfaceChunk, frame: number): ChunkState[];
|