@adatechnology/game-crowd-three 0.3.8 → 0.4.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/dist/chunk-PXT6DYGJ.js +1707 -0
- package/dist/chunk-PXT6DYGJ.js.map +1 -0
- package/dist/index.d.ts +12 -291
- package/dist/index.global.js +8899 -4990
- package/dist/index.global.js.map +1 -1
- package/dist/index.js +30 -1075
- package/dist/index.js.map +1 -1
- package/dist/testing-sTAa84Ib.d.ts +442 -0
- package/dist/testing.d.ts +7 -0
- package/dist/testing.js +19 -0
- package/dist/testing.js.map +1 -0
- package/package.json +14 -9
|
@@ -0,0 +1,1707 @@
|
|
|
1
|
+
// src/bench/module.ts
|
|
2
|
+
import { CROWD_PHYSICS_DEFAULTS, HERD_ROLES, STATE as STATE3, bindInfection, carrionCenter, createCivilians, createCrowd, createFlock, createHerd, createRng } from "@adatechnology/game-crowd";
|
|
3
|
+
import { createPlayer } from "@adatechnology/game-player";
|
|
4
|
+
|
|
5
|
+
// src/audio/bank.ts
|
|
6
|
+
import { DISTANCE_CURVES } from "@adatechnology/game-audio";
|
|
7
|
+
var scream = {
|
|
8
|
+
id: "scream_hoarse",
|
|
9
|
+
bus: "voices",
|
|
10
|
+
order: "scream",
|
|
11
|
+
duration: [1, 1.4],
|
|
12
|
+
layers: [
|
|
13
|
+
{
|
|
14
|
+
source: { kind: "noise", offset: "random" },
|
|
15
|
+
filters: {
|
|
16
|
+
mode: "parallel",
|
|
17
|
+
bank: [
|
|
18
|
+
{ type: "bandpass", q: 6, frequency: { from: 700 * 1.15, to: 700 * 0.8, curve: "linear", over: "duration" }, level: 1 },
|
|
19
|
+
{ type: "bandpass", q: 8, frequency: { from: 1150 * 1.15, to: 1150 * 0.8, curve: "linear", over: "duration" }, level: 0.7 },
|
|
20
|
+
{ type: "bandpass", q: 10, frequency: { from: 2600 * 1.15, to: 2600 * 0.8, curve: "linear", over: "duration" }, level: 0.35 }
|
|
21
|
+
]
|
|
22
|
+
},
|
|
23
|
+
modulators: [{ target: "gain", wave: "square", rate: [31, 40], depth: 0.35 }],
|
|
24
|
+
envelope: [
|
|
25
|
+
[0, 0, "set"],
|
|
26
|
+
[0.06, 0.9, "linear"],
|
|
27
|
+
["0.6d", 0.9, "set"],
|
|
28
|
+
["1d", 1e-3, "exp", "fixed"]
|
|
29
|
+
]
|
|
30
|
+
}
|
|
31
|
+
],
|
|
32
|
+
falloff: DISTANCE_CURVES.game_scream,
|
|
33
|
+
sources: ["jogo:L62128-62142"]
|
|
34
|
+
};
|
|
35
|
+
var groanLayer = (base) => ({
|
|
36
|
+
source: {
|
|
37
|
+
kind: "osc",
|
|
38
|
+
wave: "sawtooth",
|
|
39
|
+
frequency: typeof base === "number" ? { from: base, to: base * 0.7, curve: "linear", over: "duration" } : { from: base, to: { ...base, scale: 0.7 }, curve: "linear", over: "duration" }
|
|
40
|
+
},
|
|
41
|
+
filters: { mode: "serial", bank: [{ type: "bandpass", q: 3, frequency: { set: typeof base === "number" ? base * 3 : { ...base, scale: 3 } } }] },
|
|
42
|
+
envelope: [
|
|
43
|
+
[0, 0, "set"],
|
|
44
|
+
[0.15, 0.25, "linear"],
|
|
45
|
+
["1d", 1e-3, "exp", "fixed"]
|
|
46
|
+
]
|
|
47
|
+
});
|
|
48
|
+
var groan = {
|
|
49
|
+
id: "groan",
|
|
50
|
+
bus: "voices",
|
|
51
|
+
order: "groan",
|
|
52
|
+
vars: { base: [95, 135], dur: [0.9, 1.5] },
|
|
53
|
+
duration: { param: "dur" },
|
|
54
|
+
layers: [groanLayer({ param: "base" })],
|
|
55
|
+
falloff: DISTANCE_CURVES.game_voice,
|
|
56
|
+
sources: ["jogo:L62143-62155 gemido"]
|
|
57
|
+
};
|
|
58
|
+
var runnerGroan = {
|
|
59
|
+
id: "runner_groan",
|
|
60
|
+
bus: "voices",
|
|
61
|
+
order: "groan",
|
|
62
|
+
vars: { dur: [0.9, 1.5] },
|
|
63
|
+
duration: { param: "dur" },
|
|
64
|
+
layers: [groanLayer(300)],
|
|
65
|
+
falloff: DISTANCE_CURVES.game_voice,
|
|
66
|
+
sources: ["jogo:L62143-62155 corredor"]
|
|
67
|
+
};
|
|
68
|
+
var call = (id, options) => ({
|
|
69
|
+
id,
|
|
70
|
+
bus: "world",
|
|
71
|
+
order: "calls",
|
|
72
|
+
duration: options.seconds,
|
|
73
|
+
tail: 0.02,
|
|
74
|
+
keepSilent: true,
|
|
75
|
+
layers: [
|
|
76
|
+
{
|
|
77
|
+
repeat: { count: options.count, spacing: options.spacing },
|
|
78
|
+
source: { kind: "osc", wave: options.wave, frequency: { from: options.frequency, to: options.frequency * options.drop, curve: "exp", over: "duration" } },
|
|
79
|
+
filters: { mode: "serial", bank: [{ type: "bandpass", frequency: { set: options.frequency * 1.2 }, q: 3 }] },
|
|
80
|
+
envelope: [
|
|
81
|
+
[0, 1e-4, "set", "fixed"],
|
|
82
|
+
[0.015, 1, "exp"],
|
|
83
|
+
["1d", 1e-4, "exp", "fixed"]
|
|
84
|
+
]
|
|
85
|
+
}
|
|
86
|
+
],
|
|
87
|
+
falloff: DISTANCE_CURVES.animals,
|
|
88
|
+
sources: ["jogo:L66625-66639"]
|
|
89
|
+
});
|
|
90
|
+
var CROWD_SOUND_BANK = {
|
|
91
|
+
id: "crowd",
|
|
92
|
+
sounds: [
|
|
93
|
+
scream,
|
|
94
|
+
groan,
|
|
95
|
+
runnerGroan,
|
|
96
|
+
call("dog_bark", { count: 2, spacing: 0.22, seconds: 0.12, wave: "square", frequency: 520, drop: 0.6 }),
|
|
97
|
+
call("lapwing_call", { count: 3, spacing: 0.14, seconds: 0.09, wave: "square", frequency: 2400, drop: 0.6 }),
|
|
98
|
+
call("hen_cluck", { count: 4, spacing: 0.14, seconds: 0.09, wave: "square", frequency: 900, drop: 0.6 }),
|
|
99
|
+
call("cow_moo", { count: 1, spacing: 0.14, seconds: 1.2, wave: "sawtooth", frequency: 140, drop: 0.8 })
|
|
100
|
+
]
|
|
101
|
+
};
|
|
102
|
+
var HERD_SOUND_IDS = { bark: "dog_bark", lapwing: "lapwing_call", cluck: "hen_cluck", moo: "cow_moo" };
|
|
103
|
+
|
|
104
|
+
// src/herd-view.ts
|
|
105
|
+
function createHerdView(three, options) {
|
|
106
|
+
const group = new three.Group();
|
|
107
|
+
group.name = "herd";
|
|
108
|
+
const drawn = /* @__PURE__ */ new Map();
|
|
109
|
+
const sky = [];
|
|
110
|
+
const build = (E) => {
|
|
111
|
+
const species = options.species(E.spec.species);
|
|
112
|
+
if (!species) return null;
|
|
113
|
+
const rig = options.drawer.build(species);
|
|
114
|
+
rig.root.position.set(E.x, 0, E.z);
|
|
115
|
+
if (E.dead) options.drawer.setVisualState?.(rig, "dead");
|
|
116
|
+
group.add(rig.root);
|
|
117
|
+
drawn.set(E, rig);
|
|
118
|
+
if (E.spec.sky) sky.push(E);
|
|
119
|
+
return rig;
|
|
120
|
+
};
|
|
121
|
+
return {
|
|
122
|
+
group,
|
|
123
|
+
update(dt) {
|
|
124
|
+
for (const E of options.herd.list) if (!drawn.has(E)) build(E);
|
|
125
|
+
const time = options.herd.time;
|
|
126
|
+
for (const [E, rig] of drawn) {
|
|
127
|
+
if (E.spec.sky) continue;
|
|
128
|
+
rig.root.position.set(E.x, 0, E.z);
|
|
129
|
+
rig.root.rotation.y = E.yaw;
|
|
130
|
+
options.drawer.animate(rig, { time, delta: dt, clip: E.clip });
|
|
131
|
+
}
|
|
132
|
+
sky.forEach((E, i) => {
|
|
133
|
+
const rig = drawn.get(E);
|
|
134
|
+
const U = options.flock?.birds[i];
|
|
135
|
+
if (!rig || !U) return;
|
|
136
|
+
rig.root.position.set(U.x, U.y, U.z);
|
|
137
|
+
rig.root.rotation.set(0, U.yaw, U.roll);
|
|
138
|
+
rig.root.visible = U.visible;
|
|
139
|
+
if (!U.visible) return;
|
|
140
|
+
rig.phase = U.wing;
|
|
141
|
+
for (const w of rig.wings) {
|
|
142
|
+
w.shoulder.rotation.y = 0;
|
|
143
|
+
w.elbow.rotation.y = 0;
|
|
144
|
+
w.shoulder.rotation.z = w.side * (0.18 + Math.sin(U.wing * 0.4) * 0.05);
|
|
145
|
+
}
|
|
146
|
+
});
|
|
147
|
+
},
|
|
148
|
+
get count() {
|
|
149
|
+
return drawn.size;
|
|
150
|
+
},
|
|
151
|
+
clear() {
|
|
152
|
+
for (const rig of drawn.values()) group.remove(rig.root);
|
|
153
|
+
drawn.clear();
|
|
154
|
+
sky.length = 0;
|
|
155
|
+
}
|
|
156
|
+
};
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
// src/instances.ts
|
|
160
|
+
import { INSTANCE_POSES, walkGait } from "@adatechnology/game-characters";
|
|
161
|
+
import { STAGGER_POSE } from "@adatechnology/game-characters-three";
|
|
162
|
+
import { MISSING, STATE } from "@adatechnology/game-crowd";
|
|
163
|
+
|
|
164
|
+
// src/data/render.json
|
|
165
|
+
var render_default = {
|
|
166
|
+
sources: {
|
|
167
|
+
agents: "jogo.js G 67098-67121 hordeRender: view cull (behind beyond 0.35\xB7d + 3 m once past \u221A30 m, nothing past 200 m), LOD [18, 55] m with 1.5 m of hysteresis, attack pose in at 1.6 m and out at 2.2 m, the pose by state, the bloater's swelling, the eye only with the skull and awake",
|
|
168
|
+
corpses: "jogo.js G 67122-67131 hordeRender: an agent's body on its variant at the near level within 18 m (else the middle), the fallen pose, the colour darkening 70% over the body's life, the last 60 s shrinking it; a body that was not an agent on the plain body batch",
|
|
169
|
+
civilians: "jogo.js G 70177-70184 npcRender: one baked body per survivor, a child 0.66 high and 0.75 wide, the bite showing as 0.3 of age",
|
|
170
|
+
batches: "jogo.js G 67079-67094 ZVAR, CB: each variant at 1, 0.5 and 0.28 of detail in batches of 160, 700 and 2600; the plain corpse batch at the middle body (BODY_LOD[1]) for 400; G 68469 PB: survivors baked at 0.8 in batches of 4"
|
|
171
|
+
},
|
|
172
|
+
numbers: {
|
|
173
|
+
lodDistances: [18, 55],
|
|
174
|
+
lodHysteresis: 1.5,
|
|
175
|
+
behindFrom: 30,
|
|
176
|
+
behindFactor: 0.35,
|
|
177
|
+
behindMargin: 3,
|
|
178
|
+
farCull: 200,
|
|
179
|
+
gaitMax: 1.4,
|
|
180
|
+
attackEnter: 2.56,
|
|
181
|
+
attackLeave: 4.84,
|
|
182
|
+
swell: 0.9,
|
|
183
|
+
swellPivot: 0.5,
|
|
184
|
+
ageBase: 0.75,
|
|
185
|
+
agePer: 0.08,
|
|
186
|
+
armBase: 1,
|
|
187
|
+
armPer: 0.4,
|
|
188
|
+
corpseNear: 18,
|
|
189
|
+
corpseFade: 60,
|
|
190
|
+
corpseDarken: 0.7,
|
|
191
|
+
corpseRise: 1.8,
|
|
192
|
+
corpseAgeBase: 0.8,
|
|
193
|
+
corpseAgeDark: 0.2,
|
|
194
|
+
childHeight: 0.66,
|
|
195
|
+
childBuild: 0.75,
|
|
196
|
+
bittenLook: 0.3,
|
|
197
|
+
civilianSkinStep: 0.25,
|
|
198
|
+
civilianBake: 0.8,
|
|
199
|
+
civilianBatch: 4
|
|
200
|
+
},
|
|
201
|
+
poses: {
|
|
202
|
+
dormant: "fallen",
|
|
203
|
+
airborne: "leap",
|
|
204
|
+
eating: "eat",
|
|
205
|
+
pileStep: "attack",
|
|
206
|
+
swing: "scream",
|
|
207
|
+
crawl: "crawl",
|
|
208
|
+
grab: "attack",
|
|
209
|
+
calling: "scream",
|
|
210
|
+
default: "walk",
|
|
211
|
+
corpse: "fallen",
|
|
212
|
+
civilian: "walk"
|
|
213
|
+
}
|
|
214
|
+
};
|
|
215
|
+
|
|
216
|
+
// src/variety/look.ts
|
|
217
|
+
import { BALANCE, unit } from "@adatechnology/game-crowd";
|
|
218
|
+
import { ACCESSORY_BIT, packIdentity } from "@adatechnology/game-characters-three";
|
|
219
|
+
var VARIETY_DEFAULTS = {
|
|
220
|
+
capChance: 0.16,
|
|
221
|
+
backpackChance: 0.2,
|
|
222
|
+
hoodChance: 0.1,
|
|
223
|
+
/** Chance of a wound in a region at the youngest infection age, and how much each age step adds. */
|
|
224
|
+
woundBase: 0.05,
|
|
225
|
+
woundPerAge: 0.12,
|
|
226
|
+
/** Share of the wounds that are severe. */
|
|
227
|
+
severeShare: 0.35,
|
|
228
|
+
/** Steps of the infection age of the pack (0 .. ageSteps). */
|
|
229
|
+
ageSteps: 3,
|
|
230
|
+
/** How much of a shove's push (0..1) tilts the body. */
|
|
231
|
+
leanScale: 0.3
|
|
232
|
+
};
|
|
233
|
+
var LOOK_SALT = { TROUSERS: 499969, CAP: 500226, BACKPACK: 500483, HOOD: 500740, WOUND: 500997, FRESH: 501254 };
|
|
234
|
+
var REGIONS = 6;
|
|
235
|
+
var TROUSER_COUNT = 16;
|
|
236
|
+
var WOUND_BITS = 2;
|
|
237
|
+
var FRESHEST = 15;
|
|
238
|
+
var FRESH_STEPS = 12;
|
|
239
|
+
var LEAN_MIN = 0.02;
|
|
240
|
+
var woundsOf = (store, i, identity, ageNorm, N) => {
|
|
241
|
+
const own = store.wounds[i];
|
|
242
|
+
const chance = N.woundBase + N.woundPerAge * ageNorm;
|
|
243
|
+
let wounds = 0;
|
|
244
|
+
for (let region = 0; region < REGIONS; region++) {
|
|
245
|
+
const draw = unit(identity, region, LOOK_SALT.WOUND);
|
|
246
|
+
const drawn = draw < chance * N.severeShare ? 2 : draw < chance ? 1 : 0;
|
|
247
|
+
wounds |= Math.max(own >> region * WOUND_BITS & 3, drawn) << region * WOUND_BITS;
|
|
248
|
+
}
|
|
249
|
+
return wounds;
|
|
250
|
+
};
|
|
251
|
+
var accessoriesOf = (store, i, identity, N) => {
|
|
252
|
+
let bits = store.helmet[i] ? ACCESSORY_BIT.HELMET : unit(identity, 0, LOOK_SALT.CAP) < N.capChance ? ACCESSORY_BIT.CAP : 0;
|
|
253
|
+
if (!bits && unit(identity, 0, LOOK_SALT.HOOD) < N.hoodChance) bits |= ACCESSORY_BIT.EXTRA;
|
|
254
|
+
if (unit(identity, 0, LOOK_SALT.BACKPACK) < N.backpackChance) bits |= ACCESSORY_BIT.BACKPACK;
|
|
255
|
+
return bits;
|
|
256
|
+
};
|
|
257
|
+
var leanOf = (store, i, N) => {
|
|
258
|
+
const lx = store.leanX[i];
|
|
259
|
+
const lz = store.leanZ[i];
|
|
260
|
+
const size = Math.min(1, Math.hypot(lx, lz));
|
|
261
|
+
if (size < LEAN_MIN) return { leanAngle: 0, leanAmount: 0 };
|
|
262
|
+
const yaw = store.renderYaw[i];
|
|
263
|
+
const cos = Math.cos(yaw);
|
|
264
|
+
const sin = Math.sin(yaw);
|
|
265
|
+
return { leanAngle: Math.atan2(cos * lx - sin * lz, sin * lx + cos * lz), leanAmount: size * N.leanScale };
|
|
266
|
+
};
|
|
267
|
+
var lookOfAgent = (store, i, N, extra = {}) => {
|
|
268
|
+
const identity = store.identity[i];
|
|
269
|
+
const ageNorm = Math.min(1, store.age[i] / N.ageSteps);
|
|
270
|
+
const wounds = woundsOf(store, i, identity, ageNorm, N);
|
|
271
|
+
const fresh = store.wounds[i] ? FRESHEST : Math.max(0, Math.min(FRESHEST, FRESHEST - Math.round(ageNorm * FRESH_STEPS) + Math.floor(unit(identity, 0, LOOK_SALT.FRESH) * 4)));
|
|
272
|
+
return {
|
|
273
|
+
wounds,
|
|
274
|
+
freshness: fresh,
|
|
275
|
+
trousers: Math.floor(unit(identity, 0, LOOK_SALT.TROUSERS) * TROUSER_COUNT),
|
|
276
|
+
gaitStyle: store.gaitStyle[i],
|
|
277
|
+
accessories: accessoriesOf(store, i, identity, N),
|
|
278
|
+
...leanOf(store, i, N),
|
|
279
|
+
...extra
|
|
280
|
+
};
|
|
281
|
+
};
|
|
282
|
+
var isStaggering = (store, i) => store.balance[i] === BALANCE.STAGGERING;
|
|
283
|
+
var packLook = (look) => packIdentity(look);
|
|
284
|
+
|
|
285
|
+
// src/instances.ts
|
|
286
|
+
var CROWD_RENDER_DEFAULTS = {
|
|
287
|
+
sources: render_default.sources,
|
|
288
|
+
numbers: render_default.numbers,
|
|
289
|
+
poses: render_default.poses
|
|
290
|
+
};
|
|
291
|
+
var IMPOSTOR_LEVEL = 3;
|
|
292
|
+
var BUDGET_BIN = 0.25;
|
|
293
|
+
var SORT_BINS = 130;
|
|
294
|
+
var sortBin = (distance) => distance < 0 ? 0 : Math.min(SORT_BINS - 1, 1 + Math.floor(distance * 2));
|
|
295
|
+
var CORPSE_REACH = 60;
|
|
296
|
+
var EDGE_SHRINK = 0.3;
|
|
297
|
+
var EDGE_GROW = 0.15;
|
|
298
|
+
var poseIndex = (pose) => INSTANCE_POSES.indexOf(pose);
|
|
299
|
+
var STAGGER_FAMILY = 6;
|
|
300
|
+
var clipsOf = (store, i, staggering) => {
|
|
301
|
+
const family = staggering ? STAGGER_FAMILY : Math.min(store.gaitStyle[i], STAGGER_FAMILY - 1);
|
|
302
|
+
return { clips: true, clipA: family * 2, clipB: family * 2 + 1, clipBlend: Math.min(1, Math.max(0, store.arms[i])) };
|
|
303
|
+
};
|
|
304
|
+
var lookFields = (store, i, numbers, clips, staggering) => {
|
|
305
|
+
const look = lookOfAgent(store, i, numbers, clips ? clipsOf(store, i, staggering) : {});
|
|
306
|
+
return { d: packLook(look), accessories: look.accessories, type: store.type[i] };
|
|
307
|
+
};
|
|
308
|
+
function createCrowdInstances(options) {
|
|
309
|
+
const N = { ...render_default.numbers, ...options.numbers };
|
|
310
|
+
const P = { ...render_default.poses, ...options.poses };
|
|
311
|
+
const pose = {
|
|
312
|
+
dormant: poseIndex(P.dormant),
|
|
313
|
+
airborne: poseIndex(P.airborne),
|
|
314
|
+
eating: poseIndex(P.eating),
|
|
315
|
+
pileStep: poseIndex(P.pileStep),
|
|
316
|
+
swing: poseIndex(P.swing),
|
|
317
|
+
crawl: poseIndex(P.crawl),
|
|
318
|
+
grab: poseIndex(P.grab),
|
|
319
|
+
calling: poseIndex(P.calling),
|
|
320
|
+
walk: poseIndex(P.default),
|
|
321
|
+
corpse: poseIndex(P.corpse),
|
|
322
|
+
civilian: poseIndex(P.civilian)
|
|
323
|
+
};
|
|
324
|
+
const [near, middle] = N.lodDistances;
|
|
325
|
+
const variety = options.variety ? { ...VARIETY_DEFAULTS, ...options.variety.numbers } : null;
|
|
326
|
+
const impostorFrom = options.variety?.impostorFrom;
|
|
327
|
+
const segmentClips = options.variety?.segmentClips === true;
|
|
328
|
+
const lodEdges = impostorFrom === void 0 ? [near, middle] : [near, middle, impostorFrom];
|
|
329
|
+
const lastLevel = lodEdges.length;
|
|
330
|
+
const budget = options.variety?.budget;
|
|
331
|
+
const edges = [...lodEdges];
|
|
332
|
+
const bands = lodEdges.map(() => N.lodHysteresis);
|
|
333
|
+
const histogram = new Uint32Array(budget ? Math.max(1, Math.ceil(lodEdges[lastLevel - 1] / BUDGET_BIN) + 1) : 0);
|
|
334
|
+
let distances = new Float32Array(0);
|
|
335
|
+
let order = new Uint32Array(0);
|
|
336
|
+
const sortStarts = new Uint32Array(SORT_BINS + 1);
|
|
337
|
+
const behindFrom = N.behindFrom;
|
|
338
|
+
const farSquared = N.farCull * N.farCull;
|
|
339
|
+
const fitEdges = (scale) => {
|
|
340
|
+
let allowed = 0;
|
|
341
|
+
let seen = 0;
|
|
342
|
+
let bin = 0;
|
|
343
|
+
let previous = 0;
|
|
344
|
+
for (let level = 0; level < lastLevel; level++) {
|
|
345
|
+
allowed += Math.round((budget[level] ?? Infinity) * scale);
|
|
346
|
+
while (bin < histogram.length && seen + histogram[bin] <= allowed) seen += histogram[bin++];
|
|
347
|
+
const reach = bin < histogram.length ? bin * BUDGET_BIN : lodEdges[level];
|
|
348
|
+
const target = Math.max(previous, Math.min(lodEdges[level], reach));
|
|
349
|
+
edges[level] = Math.min(edges[level] + EDGE_GROW, Math.max(edges[level] - EDGE_SHRINK, target));
|
|
350
|
+
bands[level] = N.lodHysteresis * Math.min(1, edges[level] / lodEdges[level]);
|
|
351
|
+
previous = target;
|
|
352
|
+
}
|
|
353
|
+
};
|
|
354
|
+
const nearestFirst = (count) => {
|
|
355
|
+
sortStarts.fill(0);
|
|
356
|
+
for (let i = 0; i < count; i++) sortStarts[sortBin(distances[i]) + 1]++;
|
|
357
|
+
for (let b = 0; b < SORT_BINS; b++) sortStarts[b + 1] += sortStarts[b];
|
|
358
|
+
for (let i = 0; i < count; i++) order[sortStarts[sortBin(distances[i])]++] = i;
|
|
359
|
+
};
|
|
360
|
+
const corpseHistogram = new Uint32Array(budget ? Math.ceil(CORPSE_REACH / BUDGET_BIN) + 1 : 0);
|
|
361
|
+
const fitCorpseEdges = (entries, cx, cz, scale) => {
|
|
362
|
+
if (!budget) return [N.corpseNear, Infinity];
|
|
363
|
+
corpseHistogram.fill(0);
|
|
364
|
+
for (const { data } of entries) {
|
|
365
|
+
const distance = Math.hypot(data.x - cx, data.z - cz);
|
|
366
|
+
if (distance < CORPSE_REACH) corpseHistogram[Math.floor(distance / BUDGET_BIN)]++;
|
|
367
|
+
}
|
|
368
|
+
let seen = 0;
|
|
369
|
+
let bin = 0;
|
|
370
|
+
const edgeFor = (allowed, nominal) => {
|
|
371
|
+
while (bin < corpseHistogram.length && seen + corpseHistogram[bin] <= allowed) seen += corpseHistogram[bin++];
|
|
372
|
+
return bin < corpseHistogram.length ? Math.min(nominal, bin * BUDGET_BIN) : nominal;
|
|
373
|
+
};
|
|
374
|
+
const near2 = edgeFor(Math.round((budget[3] ?? (budget[0] ?? Infinity) / 2) * scale), N.corpseNear);
|
|
375
|
+
const middle2 = edgeFor(Math.round(((budget[3] ?? (budget[0] ?? Infinity) / 2) + (budget[4] ?? (budget[1] ?? Infinity) / 2)) * scale), Infinity);
|
|
376
|
+
return [near2, Math.max(near2, middle2)];
|
|
377
|
+
};
|
|
378
|
+
return {
|
|
379
|
+
numbers: N,
|
|
380
|
+
agents(Z, variants, frame, sink) {
|
|
381
|
+
const cx = frame.camera.x;
|
|
382
|
+
const cz = frame.camera.z;
|
|
383
|
+
const fx = -Math.sin(frame.viewYaw);
|
|
384
|
+
const fz = -Math.cos(frame.viewYaw);
|
|
385
|
+
const target = frame.target;
|
|
386
|
+
let written = 0;
|
|
387
|
+
if (distances.length < Z.n) distances = new Float32Array(Math.max(Z.n, distances.length * 2));
|
|
388
|
+
if (budget) histogram.fill(0);
|
|
389
|
+
for (let i = 0; i < Z.n; i++) {
|
|
390
|
+
const dx = Z.x[i] - cx;
|
|
391
|
+
const dz = Z.z[i] - cz;
|
|
392
|
+
const d2 = dx * dx + dz * dz;
|
|
393
|
+
distances[i] = -1;
|
|
394
|
+
if (d2 > behindFrom && dx * fx + dz * fz < -Math.sqrt(d2) * N.behindFactor - N.behindMargin) continue;
|
|
395
|
+
if (d2 > farSquared) continue;
|
|
396
|
+
if (frame.hidden?.(i)) continue;
|
|
397
|
+
const seen = Math.sqrt(d2);
|
|
398
|
+
distances[i] = seen;
|
|
399
|
+
if (budget && seen < lodEdges[lastLevel - 1]) histogram[Math.floor(seen / BUDGET_BIN)]++;
|
|
400
|
+
}
|
|
401
|
+
if (budget) fitEdges(frame.detailScale ?? 1);
|
|
402
|
+
if (budget) {
|
|
403
|
+
if (order.length < Z.n) order = new Uint32Array(distances.length);
|
|
404
|
+
nearestFirst(Z.n);
|
|
405
|
+
}
|
|
406
|
+
for (let k = 0; k < Z.n; k++) {
|
|
407
|
+
const i = budget ? order[k] : k;
|
|
408
|
+
const dd = distances[i];
|
|
409
|
+
if (dd < 0) continue;
|
|
410
|
+
let l = Z.lod[i];
|
|
411
|
+
while (l > 0 && dd < edges[l - 1] - bands[l - 1]) l--;
|
|
412
|
+
while (l < lastLevel && dd > edges[l] + bands[l]) l++;
|
|
413
|
+
Z.lod[i] = l;
|
|
414
|
+
const sp = Z.renderSpeed[i];
|
|
415
|
+
const gait = Math.min(N.gaitMax, walkGait(sp, Z.height[i]));
|
|
416
|
+
const st = Z.state[i];
|
|
417
|
+
if (st === STATE.CHASE && target?.alive) {
|
|
418
|
+
const px = Z.x[i] - target.x;
|
|
419
|
+
const pz = Z.z[i] - target.z;
|
|
420
|
+
const p2 = px * px + pz * pz;
|
|
421
|
+
Z.attackPose[i] = p2 < (Z.attackPose[i] ? N.attackLeave : N.attackEnter) ? 1 : 0;
|
|
422
|
+
} else Z.attackPose[i] = 0;
|
|
423
|
+
const p = st === STATE.DORMANT ? pose.dormant : st === STATE.AIRBORNE ? pose.airborne : st === STATE.EATING ? pose.eating : st === STATE.PILE_STEP ? pose.pileStep : st === STATE.SWING ? pose.swing : Z.crawl[i] ? pose.crawl : st === STATE.GRAB || Z.attackPose[i] ? pose.grab : st === STATE.CALLING ? pose.calling : pose.walk;
|
|
424
|
+
const staggering = variety !== null && p === pose.walk && isStaggering(Z, i);
|
|
425
|
+
const bb = st === STATE.SWELLING ? Z.build[i] * (1 + (N.swellPivot - Z.stateTimer[i]) * N.swell) : Z.build[i];
|
|
426
|
+
const eyeOn = !(Z.missing[i] & MISSING.SKULL);
|
|
427
|
+
const V = variants[Z.variant[i]];
|
|
428
|
+
const level = l === IMPOSTOR_LEVEL && p !== pose.walk ? IMPOSTOR_LEVEL - 1 : l;
|
|
429
|
+
sink(Z.variant[i], level, {
|
|
430
|
+
a: [Z.x[i], Z.z[i], Z.renderYaw[i], Z.phase[i]],
|
|
431
|
+
b: [Z.height[i] / V.s, bb / V.bulk, gait, staggering ? STAGGER_POSE : p],
|
|
432
|
+
c: [Z.missing[i], N.ageBase + Z.age[i] * N.agePer, N.armBase + N.armPer * Z.arms[i], Z.skin[i]],
|
|
433
|
+
color: Z.color[i],
|
|
434
|
+
shade: 1,
|
|
435
|
+
eye: eyeOn && st !== STATE.DORMANT ? options.eyes[Z.type[i]] ?? null : null,
|
|
436
|
+
y: Z.y[i],
|
|
437
|
+
...variety ? lookFields(Z, i, variety, segmentClips, staggering) : {}
|
|
438
|
+
});
|
|
439
|
+
written++;
|
|
440
|
+
}
|
|
441
|
+
return written;
|
|
442
|
+
},
|
|
443
|
+
corpses(entries, variants, frame, life, sink, plain) {
|
|
444
|
+
const cx = frame.camera.x;
|
|
445
|
+
const cz = frame.camera.z;
|
|
446
|
+
const [nearEdge, middleEdge] = fitCorpseEdges(entries, cx, cz, frame.detailScale ?? 1);
|
|
447
|
+
for (const { age: t, data: C } of entries) {
|
|
448
|
+
const dark = Math.min(1, t / life);
|
|
449
|
+
const shade = 1 - dark * N.corpseDarken;
|
|
450
|
+
const fade = t > life - N.corpseFade ? (life - t) / N.corpseFade : 1;
|
|
451
|
+
const c = [C.missing, N.corpseAgeBase + dark * N.corpseAgeDark, 1, C.skin];
|
|
452
|
+
if (C.variant >= 0) {
|
|
453
|
+
const V = variants[C.variant];
|
|
454
|
+
const dx = C.x - cx;
|
|
455
|
+
const dz = C.z - cz;
|
|
456
|
+
const distance = Math.hypot(dx, dz);
|
|
457
|
+
const l = distance < nearEdge ? 0 : distance < middleEdge ? 1 : 2;
|
|
458
|
+
sink(C.variant, l, { a: [C.x, C.z, C.yaw, C.seed], b: [C.height / V.s * fade, C.build / V.bulk, Math.min(1, t * N.corpseRise), pose.corpse], c, color: C.color, shade, eye: null, y: 0 });
|
|
459
|
+
continue;
|
|
460
|
+
}
|
|
461
|
+
plain({ a: [C.x, C.z, C.yaw, C.seed], b: [C.height * fade, C.build, Math.min(1, t * N.corpseRise), pose.corpse], c, color: C.color, shade, eye: null, y: 0 });
|
|
462
|
+
}
|
|
463
|
+
},
|
|
464
|
+
civilians(list, colorOf, sink) {
|
|
465
|
+
for (const n of list) {
|
|
466
|
+
if (n.state === "undead" || n.state === "gone" || n.state === "dead") continue;
|
|
467
|
+
const sp = Math.hypot(n.vx, n.vz);
|
|
468
|
+
const child = !!n.spec.child;
|
|
469
|
+
sink(n, {
|
|
470
|
+
a: [n.x, n.z, n.yaw, n.phase],
|
|
471
|
+
b: [child ? N.childHeight : 1, child ? N.childBuild : 1, Math.min(N.gaitMax, walkGait(sp, child ? N.childHeight : 1)), pose.civilian],
|
|
472
|
+
c: [0, n.biteTimer > 0 ? N.bittenLook : 0, 0, n.k * N.civilianSkinStep],
|
|
473
|
+
color: colorOf(n),
|
|
474
|
+
shade: 1,
|
|
475
|
+
eye: null,
|
|
476
|
+
y: 0
|
|
477
|
+
});
|
|
478
|
+
}
|
|
479
|
+
}
|
|
480
|
+
};
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
// src/renderer.ts
|
|
484
|
+
import { variantDecay, variantSeed } from "@adatechnology/game-characters";
|
|
485
|
+
import { markKitOrigin } from "@adatechnology/game-contracts";
|
|
486
|
+
import { KIT_ACCESSORIES, createSegmentClipUniforms, LOD_BAKE, LOD_SEGMENTS, accessoryPlacement, bakeBody, bakeImpostorAtlas, buildInstancedBody, buildKitAccessory, createBlobShadowBatch, createBodyBatch, createImpostorBatch, createVariantBatches } from "@adatechnology/game-characters-three";
|
|
487
|
+
|
|
488
|
+
// src/gpu-cost.ts
|
|
489
|
+
var NANOSECONDS_PER_MILLISECOND = 1e6;
|
|
490
|
+
var MAX_PENDING = 512;
|
|
491
|
+
function createGpuCostProbe() {
|
|
492
|
+
let context = null;
|
|
493
|
+
let timer = null;
|
|
494
|
+
let isReady = false;
|
|
495
|
+
let active = null;
|
|
496
|
+
let drawFrame = 0;
|
|
497
|
+
let lastRenderFrame = -1;
|
|
498
|
+
const pending = [];
|
|
499
|
+
const free = [];
|
|
500
|
+
let sumFrame = -1;
|
|
501
|
+
let sum = 0;
|
|
502
|
+
let last;
|
|
503
|
+
const begin = (renderer) => {
|
|
504
|
+
if (!isReady) {
|
|
505
|
+
isReady = true;
|
|
506
|
+
const gl = renderer.getContext();
|
|
507
|
+
const extension = gl.getExtension("EXT_disjoint_timer_query_webgl2");
|
|
508
|
+
if (extension) {
|
|
509
|
+
context = gl;
|
|
510
|
+
timer = extension;
|
|
511
|
+
}
|
|
512
|
+
}
|
|
513
|
+
if (!context || !timer || active || pending.length >= MAX_PENDING) return;
|
|
514
|
+
const frame = renderer.info.render.frame;
|
|
515
|
+
if (frame !== lastRenderFrame) {
|
|
516
|
+
lastRenderFrame = frame;
|
|
517
|
+
drawFrame++;
|
|
518
|
+
}
|
|
519
|
+
active = free.pop() ?? context.createQuery();
|
|
520
|
+
context.beginQuery(timer.TIME_ELAPSED_EXT, active);
|
|
521
|
+
};
|
|
522
|
+
const end = () => {
|
|
523
|
+
if (!context || !timer || !active) return;
|
|
524
|
+
context.endQuery(timer.TIME_ELAPSED_EXT);
|
|
525
|
+
pending.push({ query: active, frame: drawFrame });
|
|
526
|
+
active = null;
|
|
527
|
+
};
|
|
528
|
+
const poll = () => {
|
|
529
|
+
if (!context || !timer) return void 0;
|
|
530
|
+
const disjoint = context.getParameter(timer.GPU_DISJOINT_EXT);
|
|
531
|
+
while (pending.length > 0) {
|
|
532
|
+
const { query, frame } = pending[0];
|
|
533
|
+
if (!context.getQueryParameter(query, context.QUERY_RESULT_AVAILABLE)) break;
|
|
534
|
+
pending.shift();
|
|
535
|
+
free.push(query);
|
|
536
|
+
if (disjoint) continue;
|
|
537
|
+
if (frame !== sumFrame) {
|
|
538
|
+
if (sumFrame >= 0) last = sum;
|
|
539
|
+
sumFrame = frame;
|
|
540
|
+
sum = 0;
|
|
541
|
+
}
|
|
542
|
+
sum += context.getQueryParameter(query, context.QUERY_RESULT) / NANOSECONDS_PER_MILLISECOND;
|
|
543
|
+
}
|
|
544
|
+
return last;
|
|
545
|
+
};
|
|
546
|
+
return {
|
|
547
|
+
attach(mesh) {
|
|
548
|
+
mesh.onBeforeRender = (renderer) => begin(renderer);
|
|
549
|
+
mesh.onAfterRender = () => end();
|
|
550
|
+
},
|
|
551
|
+
poll
|
|
552
|
+
};
|
|
553
|
+
}
|
|
554
|
+
|
|
555
|
+
// src/renderer.ts
|
|
556
|
+
var ACCESSORY_CAPACITY = [480, 1400];
|
|
557
|
+
var BLOB_CAPACITY = 4e3;
|
|
558
|
+
var IMPOSTOR_FROM = 90;
|
|
559
|
+
var IMPOSTOR_CAPACITY = 4e3;
|
|
560
|
+
var NEAR_SHADOWS = [true, false, false];
|
|
561
|
+
var NEAR_PREPASS = [true, false, false];
|
|
562
|
+
var BUDGET_LEVELS = [160, 320, 600];
|
|
563
|
+
var ADAPT = { gpuHighMs: 3.5, gpuLowMs: 2.2, highMs: 24, lowMs: 19, minScale: 0.1, shrinkPerSecond: 1.2, growPerSecond: 0.5, smoothing: 0.1 };
|
|
564
|
+
var now = () => typeof performance === "undefined" ? Date.now() : performance.now();
|
|
565
|
+
function createCrowdRenderer(three, options) {
|
|
566
|
+
const { crowd } = options;
|
|
567
|
+
const group = markKitOrigin(new three.Group(), { owner: "game-crowd-three", kind: "person" });
|
|
568
|
+
group.name = "crowd";
|
|
569
|
+
const time = { value: 0 };
|
|
570
|
+
const eyes = crowd.pack.types.map((type) => type.eye ?? null);
|
|
571
|
+
const variety = options.variety;
|
|
572
|
+
const accessoryKinds = variety ? variety.accessories === false ? [] : variety.accessories ?? KIT_ACCESSORIES : [];
|
|
573
|
+
const accessoryCapacity = variety?.accessoryCapacity ?? ACCESSORY_CAPACITY;
|
|
574
|
+
const nearShadows = variety ? variety.nearShadows ?? true : false;
|
|
575
|
+
const impostorOptions = variety && variety.impostor !== false ? variety.impostor ?? {} : null;
|
|
576
|
+
const budgetOptions = variety && variety.budget !== false ? variety.budget ?? {} : null;
|
|
577
|
+
const adaptOptions = budgetOptions && budgetOptions.adapt !== false ? { ...ADAPT, ...budgetOptions.adapt } : null;
|
|
578
|
+
let detailScale = 1;
|
|
579
|
+
let frameMs = 0;
|
|
580
|
+
let gpuMs = 0;
|
|
581
|
+
const probe = adaptOptions ? createGpuCostProbe() : null;
|
|
582
|
+
const tracked = /* @__PURE__ */ new WeakSet();
|
|
583
|
+
const track = (mesh) => {
|
|
584
|
+
if (!probe || tracked.has(mesh)) return;
|
|
585
|
+
tracked.add(mesh);
|
|
586
|
+
probe.attach(mesh);
|
|
587
|
+
};
|
|
588
|
+
const instances = createCrowdInstances({
|
|
589
|
+
eyes,
|
|
590
|
+
...options.numbers ? { numbers: options.numbers } : {},
|
|
591
|
+
...options.poses ? { poses: options.poses } : {},
|
|
592
|
+
...variety ? { variety: { ...variety.numbers ? { numbers: variety.numbers } : {}, ...impostorOptions ? { impostorFrom: impostorOptions.from ?? IMPOSTOR_FROM } : {}, ...variety.segmentClips ? { segmentClips: true } : {}, ...budgetOptions ? { budget: budgetOptions.levels ?? BUDGET_LEVELS } : {} } } : {}
|
|
593
|
+
});
|
|
594
|
+
const N = instances.numbers;
|
|
595
|
+
const variants = [];
|
|
596
|
+
const batches = [];
|
|
597
|
+
let plain = null;
|
|
598
|
+
const people = /* @__PURE__ */ new Map();
|
|
599
|
+
const accessoryBatches = /* @__PURE__ */ new Map();
|
|
600
|
+
const accessoryGeometry = /* @__PURE__ */ new Map();
|
|
601
|
+
const placements = /* @__PURE__ */ new Map();
|
|
602
|
+
let blobs = null;
|
|
603
|
+
let atlas = null;
|
|
604
|
+
let impostors = null;
|
|
605
|
+
let impostorCount = 0;
|
|
606
|
+
const clipUniforms = variety?.segmentClips ? createSegmentClipUniforms(three) : null;
|
|
607
|
+
let bakeMs = 0;
|
|
608
|
+
const lodCounts = [0, 0, 0];
|
|
609
|
+
const shirt = new three.Color();
|
|
610
|
+
const eyeColors = /* @__PURE__ */ new Map();
|
|
611
|
+
const eyeOf = (hex) => {
|
|
612
|
+
let color = eyeColors.get(hex);
|
|
613
|
+
if (!color) eyeColors.set(hex, color = new three.Color(hex));
|
|
614
|
+
return color;
|
|
615
|
+
};
|
|
616
|
+
const push = (batch, instance) => {
|
|
617
|
+
if (!batch) return;
|
|
618
|
+
shirt.setHex(instance.color).multiplyScalar(instance.shade);
|
|
619
|
+
batch.push({ a: instance.a, b: instance.b, c: instance.c, color: shirt, ...instance.eye !== null ? { eye: eyeOf(instance.eye) } : {}, y: instance.y, ...instance.d ? { d: instance.d } : {} });
|
|
620
|
+
};
|
|
621
|
+
const pushAccessories = (variant, level, instance) => {
|
|
622
|
+
const bits = instance.accessories ?? 0;
|
|
623
|
+
if (!bits || level > 1) return;
|
|
624
|
+
shirt.setHex(instance.color).multiplyScalar(instance.shade);
|
|
625
|
+
for (const accessory of accessoryKinds) {
|
|
626
|
+
if (!(bits & accessory.bit)) continue;
|
|
627
|
+
accessoryBatch(accessory, level).push({ a: instance.a, b: instance.b, c: instance.c, color: shirt, y: instance.y, ...instance.d ? { d: instance.d } : {}, acc: placementOf(variant, level, accessory) });
|
|
628
|
+
}
|
|
629
|
+
};
|
|
630
|
+
const accessoryBatch = (accessory, level) => {
|
|
631
|
+
const key = `${accessory.id}|${level}`;
|
|
632
|
+
const made = accessoryBatches.get(key);
|
|
633
|
+
if (made) return made;
|
|
634
|
+
let geometry = accessoryGeometry.get(accessory.id);
|
|
635
|
+
if (!geometry) accessoryGeometry.set(accessory.id, geometry = buildKitAccessory(three, accessory));
|
|
636
|
+
const batch = createBodyBatch(three, geometry, { max: accessoryCapacity[level] ?? ACCESSORY_CAPACITY[1], castShadow: level === 0, time, accessory: true, ...clipUniforms ? { segmentClips: clipUniforms } : {} });
|
|
637
|
+
accessoryBatches.set(key, batch);
|
|
638
|
+
group.add(batch.mesh);
|
|
639
|
+
return batch;
|
|
640
|
+
};
|
|
641
|
+
const placementOf = (variant, level, accessory) => {
|
|
642
|
+
const key = `${variant}|${level}|${accessory.attach}`;
|
|
643
|
+
let placed = placements.get(key);
|
|
644
|
+
if (!placed) placements.set(key, placed = accessoryPlacement(variants[variant].geometries[level], accessory.attach));
|
|
645
|
+
return placed;
|
|
646
|
+
};
|
|
647
|
+
const ensureImpostors = () => {
|
|
648
|
+
if (impostors) return impostors;
|
|
649
|
+
bake();
|
|
650
|
+
const bodies = crowd.pack.types.map((_, c) => {
|
|
651
|
+
const variant = variants[crowd.variantsOf[c][0]];
|
|
652
|
+
return { geometry: variant.geometries[2] };
|
|
653
|
+
});
|
|
654
|
+
atlas = bakeImpostorAtlas(three, bodies);
|
|
655
|
+
impostors = createImpostorBatch(three, atlas, { max: impostorOptions?.capacity ?? IMPOSTOR_CAPACITY, ...impostorOptions?.light ? { light: impostorOptions.light } : {} });
|
|
656
|
+
group.add(impostors.mesh);
|
|
657
|
+
return impostors;
|
|
658
|
+
};
|
|
659
|
+
const pushImpostor = (instance) => {
|
|
660
|
+
shirt.setHex(instance.color).multiplyScalar(instance.shade);
|
|
661
|
+
ensureImpostors().push({ a: instance.a, scale: [instance.b[0], instance.b[1]], silhouette: instance.type ?? 0, y: instance.y, c: instance.c, color: shirt, ...instance.d ? { d: instance.d } : {} });
|
|
662
|
+
impostorCount++;
|
|
663
|
+
};
|
|
664
|
+
const ensureBlobs = () => {
|
|
665
|
+
if (!blobs) {
|
|
666
|
+
blobs = createBlobShadowBatch(three, { max: BLOB_CAPACITY });
|
|
667
|
+
group.add(blobs.mesh);
|
|
668
|
+
}
|
|
669
|
+
return blobs;
|
|
670
|
+
};
|
|
671
|
+
const typeOfVariant = [];
|
|
672
|
+
crowd.variantsOf.forEach((list, c) => list.forEach((g) => typeOfVariant[g] = c));
|
|
673
|
+
const baked = /* @__PURE__ */ new Set();
|
|
674
|
+
const ensurePlain = () => {
|
|
675
|
+
if (!plain) {
|
|
676
|
+
plain = createBodyBatch(three, options.plainBody ?? buildInstancedBody(three, { segments: LOD_SEGMENTS[1] }), { max: crowd.numbers.corpsesMax, castShadow: false, time });
|
|
677
|
+
group.add(plain.mesh);
|
|
678
|
+
}
|
|
679
|
+
return plain;
|
|
680
|
+
};
|
|
681
|
+
const bakeType = (c) => {
|
|
682
|
+
if (baked.has(c)) return;
|
|
683
|
+
baked.add(c);
|
|
684
|
+
const type = crowd.pack.types[c];
|
|
685
|
+
const name = options.typeName?.(type, c) ?? type.id;
|
|
686
|
+
const started = now();
|
|
687
|
+
crowd.variantsOf[c].forEach((g, v) => {
|
|
688
|
+
const decay = variantDecay(v);
|
|
689
|
+
const seed = variantSeed(c, v);
|
|
690
|
+
const bodies = LOD_BAKE.map((lod) => bakeBody(three, options.builders, { type: name, decay, seed }, lod));
|
|
691
|
+
const variant = { typeId: type.id, variant: v, seed, decay, s: bodies[0].s, bulk: bodies[0].bulk, verts: bodies.map((body) => body.verts), geometries: bodies.map((body) => body.geo) };
|
|
692
|
+
variants[g] = variant;
|
|
693
|
+
batches[g] = createVariantBatches(three, variant, time, variety ? { identity: true, ...clipUniforms ? { segmentClips: clipUniforms } : {}, ...nearShadows ? { castsShadow: NEAR_SHADOWS } : {}, ...variety.nearDepthPrepass ?? true ? { depthPrepass: NEAR_PREPASS } : {} } : {});
|
|
694
|
+
for (const batch of batches[g]) group.add(batch.mesh, ...batch.prepass ? [batch.prepass] : []);
|
|
695
|
+
});
|
|
696
|
+
bakeMs += now() - started;
|
|
697
|
+
};
|
|
698
|
+
const bake = () => crowd.pack.types.forEach((_, c) => bakeType(c));
|
|
699
|
+
const bakeSeen = () => {
|
|
700
|
+
const Z = crowd.store;
|
|
701
|
+
for (let i = 0; i < Z.n; i++) if (!baked.has(Z.type[i])) bakeType(Z.type[i]);
|
|
702
|
+
for (const { data } of crowd.corpses.entries) if (data.variant >= 0 && !baked.has(typeOfVariant[data.variant])) bakeType(typeOfVariant[data.variant]);
|
|
703
|
+
};
|
|
704
|
+
const personBatch = (civilian) => {
|
|
705
|
+
const made = people.get(civilian.id);
|
|
706
|
+
if (made) return made;
|
|
707
|
+
const spec = options.civilians?.body(civilian);
|
|
708
|
+
if (!spec) return void 0;
|
|
709
|
+
const batch = createBodyBatch(three, bakeBody(three, options.builders, spec, N.civilianBake).geo, { max: N.civilianBatch, time });
|
|
710
|
+
people.set(civilian.id, batch);
|
|
711
|
+
group.add(batch.mesh);
|
|
712
|
+
return batch;
|
|
713
|
+
};
|
|
714
|
+
const all = () => [...batches.flatMap((set) => set ?? []), ...plain ? [plain] : [], ...people.values(), ...accessoryBatches.values()];
|
|
715
|
+
return {
|
|
716
|
+
group,
|
|
717
|
+
instances,
|
|
718
|
+
bake,
|
|
719
|
+
prepare() {
|
|
720
|
+
bake();
|
|
721
|
+
ensurePlain();
|
|
722
|
+
if (!variety) return;
|
|
723
|
+
if (variety.blobShadows ?? true) ensureBlobs();
|
|
724
|
+
if (impostorOptions) ensureImpostors();
|
|
725
|
+
for (const accessory of accessoryKinds) for (const level of [0, 1]) accessoryBatch(accessory, level);
|
|
726
|
+
},
|
|
727
|
+
update(dt, frame) {
|
|
728
|
+
if (adaptOptions && probe && dt > 0) {
|
|
729
|
+
const measured = probe.poll();
|
|
730
|
+
if (measured !== void 0) gpuMs += (measured - gpuMs) * (gpuMs === 0 ? 1 : adaptOptions.smoothing);
|
|
731
|
+
frameMs += (dt * 1e3 - frameMs) * (frameMs === 0 ? 1 : adaptOptions.smoothing);
|
|
732
|
+
const [high, low, load] = measured === void 0 ? [adaptOptions.highMs, adaptOptions.lowMs, frameMs] : [adaptOptions.gpuHighMs, adaptOptions.gpuLowMs, gpuMs];
|
|
733
|
+
if (load > high) detailScale = Math.max(adaptOptions.minScale, detailScale * (1 - adaptOptions.shrinkPerSecond * dt));
|
|
734
|
+
else if (load < low) detailScale = Math.min(1, detailScale * (1 + adaptOptions.growPerSecond * dt));
|
|
735
|
+
}
|
|
736
|
+
if (budgetOptions) frame = { ...frame, detailScale };
|
|
737
|
+
bakeSeen();
|
|
738
|
+
time.value += dt;
|
|
739
|
+
for (const batch of all()) batch.begin();
|
|
740
|
+
blobs?.begin();
|
|
741
|
+
impostors?.begin();
|
|
742
|
+
impostorCount = 0;
|
|
743
|
+
const drawBlobs = variety ? variety.blobShadows ?? true : false;
|
|
744
|
+
instances.agents(crowd.store, variants, frame, (v, l, instance) => {
|
|
745
|
+
if (l === IMPOSTOR_LEVEL) pushImpostor(instance);
|
|
746
|
+
else push(batches[v]?.[l], instance);
|
|
747
|
+
if (!variety) return;
|
|
748
|
+
pushAccessories(v, l, instance);
|
|
749
|
+
if (drawBlobs && l > 0) ensureBlobs().push(instance.a[0], instance.y, instance.a[1], instance.b[0] * (variants[v]?.s ?? 1));
|
|
750
|
+
});
|
|
751
|
+
if (options.drawCorpses?.() !== false) instances.corpses(crowd.corpses.entries, variants, frame, crowd.numbers.corpseSeconds, (v, l, instance) => push(batches[v]?.[l], instance), (instance) => push(ensurePlain(), instance));
|
|
752
|
+
for (let l = 0; l < 3; l++) lodCounts[l] = batches.reduce((sum, set) => sum + (set?.[l]?.count ?? 0), 0);
|
|
753
|
+
const list = options.civilians?.list();
|
|
754
|
+
if (list) instances.civilians(list, options.civilians?.color ?? ((n) => n.spec.color ?? 8421504), (n, instance) => push(personBatch(n), instance));
|
|
755
|
+
for (const batch of all()) batch.end();
|
|
756
|
+
blobs?.end();
|
|
757
|
+
impostors?.end();
|
|
758
|
+
if (probe) {
|
|
759
|
+
for (const batch of all()) {
|
|
760
|
+
track(batch.mesh);
|
|
761
|
+
if (batch.prepass) track(batch.prepass);
|
|
762
|
+
}
|
|
763
|
+
if (blobs) track(blobs.mesh);
|
|
764
|
+
if (impostors) track(impostors.mesh);
|
|
765
|
+
}
|
|
766
|
+
},
|
|
767
|
+
get detailScale() {
|
|
768
|
+
return detailScale;
|
|
769
|
+
},
|
|
770
|
+
get gpuMs() {
|
|
771
|
+
return gpuMs;
|
|
772
|
+
},
|
|
773
|
+
get impostorCount() {
|
|
774
|
+
return impostorCount;
|
|
775
|
+
},
|
|
776
|
+
get lodCounts() {
|
|
777
|
+
return lodCounts;
|
|
778
|
+
},
|
|
779
|
+
get drawCalls() {
|
|
780
|
+
return all().filter((batch) => batch.count > 0).length + (blobs && blobs.count > 0 ? 1 : 0) + (impostors && impostors.count > 0 ? 1 : 0);
|
|
781
|
+
},
|
|
782
|
+
get bakeMs() {
|
|
783
|
+
return bakeMs;
|
|
784
|
+
},
|
|
785
|
+
get variants() {
|
|
786
|
+
return variants;
|
|
787
|
+
},
|
|
788
|
+
dispose() {
|
|
789
|
+
for (const batch of all()) {
|
|
790
|
+
batch.mesh.geometry.dispose();
|
|
791
|
+
group.remove(batch.mesh);
|
|
792
|
+
if (batch.prepass) group.remove(batch.prepass);
|
|
793
|
+
}
|
|
794
|
+
variants.length = 0;
|
|
795
|
+
batches.length = 0;
|
|
796
|
+
baked.clear();
|
|
797
|
+
plain = null;
|
|
798
|
+
people.clear();
|
|
799
|
+
accessoryBatches.clear();
|
|
800
|
+
accessoryGeometry.clear();
|
|
801
|
+
placements.clear();
|
|
802
|
+
blobs?.mesh.geometry.dispose();
|
|
803
|
+
if (blobs) group.remove(blobs.mesh);
|
|
804
|
+
blobs = null;
|
|
805
|
+
impostors?.mesh.geometry.dispose();
|
|
806
|
+
if (impostors) group.remove(impostors.mesh);
|
|
807
|
+
atlas?.dispose();
|
|
808
|
+
clipUniforms?.uClips.value.dispose();
|
|
809
|
+
impostors = null;
|
|
810
|
+
atlas = null;
|
|
811
|
+
}
|
|
812
|
+
};
|
|
813
|
+
}
|
|
814
|
+
|
|
815
|
+
// src/bench/locale.ts
|
|
816
|
+
var EN = {
|
|
817
|
+
group: "Crowd",
|
|
818
|
+
name: "Crowd area",
|
|
819
|
+
mix: "Mixed (by weight)",
|
|
820
|
+
controls: {
|
|
821
|
+
scene: "Scene",
|
|
822
|
+
physics: "Crowd physics",
|
|
823
|
+
count: "Agents",
|
|
824
|
+
type: "Type of the new ones",
|
|
825
|
+
night: "Night",
|
|
826
|
+
seed: "Seed",
|
|
827
|
+
spawn: "Add 100",
|
|
828
|
+
noise: "Noise here (80 m)",
|
|
829
|
+
hit: "Shoot the nearest in the head",
|
|
830
|
+
kill_all: "Kill all",
|
|
831
|
+
join: "Civilians join you",
|
|
832
|
+
flee: "Civilians run away",
|
|
833
|
+
herd: "Bring the animals",
|
|
834
|
+
materialize: "Bring 50 from the region",
|
|
835
|
+
push: "Push (break a grab)",
|
|
836
|
+
reset: "Start again"
|
|
837
|
+
},
|
|
838
|
+
hints: {
|
|
839
|
+
scene: "Where the crowd is born: the open ring, a horde pressed against a wall and a door, or a horde coming down a street at a row of gas cylinders and a car. Applied before the physics and the count.",
|
|
840
|
+
physics: "Contact, pressure, falls, mounds, thrown bodies and blasts (the crowd is rebuilt with them).",
|
|
841
|
+
count: "The crowd grows or shrinks to this many, born 10 to 90 m from the area centre. Only the near ones think every frame (relevance bands).",
|
|
842
|
+
type: "The type of the agents added from now on; mixed draws them by the weights of the pack.",
|
|
843
|
+
night: "At night sight drops by 40%.",
|
|
844
|
+
noise: "A noise heard 80 m around you: the ones that hear it come.",
|
|
845
|
+
hit: "A headshot to the nearest agent: a body on the ground, blood through the room effects.",
|
|
846
|
+
join: "The civilians follow you; bitten, they turn after the bite time.",
|
|
847
|
+
flee: "The civilians run off along a path out of the area and are gone.",
|
|
848
|
+
herd: "A companion dog, hens, a lapwing, cows, a horse, a pig, a capybara, a carcass and the vultures over it.",
|
|
849
|
+
materialize: "The region model turns 50 of its numbers into agents near you; the far ones go back.",
|
|
850
|
+
push: "What holds you lets go after six pushes."
|
|
851
|
+
},
|
|
852
|
+
layers: { corpses: "Bodies on the ground", civilians: "Civilians", animals: "Animals" },
|
|
853
|
+
metrics: {
|
|
854
|
+
agents: "Agents",
|
|
855
|
+
drawn: "Drawn",
|
|
856
|
+
lod_near: "Near level",
|
|
857
|
+
lod_middle: "Middle level",
|
|
858
|
+
lod_far: "Far level",
|
|
859
|
+
draw_calls: "Draw calls",
|
|
860
|
+
bake_ms: "Bake",
|
|
861
|
+
detail_scale: "Detail budget",
|
|
862
|
+
crowd_gpu_ms: "Crowd GPU time",
|
|
863
|
+
active: "Thinking every frame",
|
|
864
|
+
kills: "Kills",
|
|
865
|
+
corpses: "Bodies",
|
|
866
|
+
civilians: "Civilians alive",
|
|
867
|
+
health: "Your health",
|
|
868
|
+
grabbed: "Grabbed",
|
|
869
|
+
message: "Last message",
|
|
870
|
+
falls_pressure: "Falls by pressure",
|
|
871
|
+
falls_chain: "Chain falls beyond a blast",
|
|
872
|
+
falls_landing: "Falls by a landing body",
|
|
873
|
+
climbers: "On the mound, peak",
|
|
874
|
+
mound_max: "Highest mound, peak",
|
|
875
|
+
thrown_peak: "Thrown bodies, peak",
|
|
876
|
+
max_overlap: "Largest overlap, peak",
|
|
877
|
+
chain_links: "Chain links",
|
|
878
|
+
crowd_ms_p99: "Crowd step, p99"
|
|
879
|
+
},
|
|
880
|
+
sceneNames: { open: "Open ring", dense_wall: "Dense wall", chain_line: "Chain line" },
|
|
881
|
+
scenarios: {
|
|
882
|
+
relevance: "Relevance: 500 agents, only the near ones every frame",
|
|
883
|
+
lod: "Level of detail with 1.5 m of hysteresis",
|
|
884
|
+
attack_pose: "Attack pose in at 1.6 m, out at 2.2 m",
|
|
885
|
+
corpse_darkens: "A body darkens and shrinks at the end of its life",
|
|
886
|
+
civilians_flee: "Civilians follow, then run away and are gone",
|
|
887
|
+
herd_calls: "The hen flees and clucks, the lapwing raises the alarm",
|
|
888
|
+
vultures: "The vultures circle the bodies",
|
|
889
|
+
infection: "Region numbers become agents and go back",
|
|
890
|
+
determinism: "The same seed, the same crowd"
|
|
891
|
+
},
|
|
892
|
+
messages: {
|
|
893
|
+
called: "A screamer called the crowd",
|
|
894
|
+
burst_hit: "Hit by a bloater burst",
|
|
895
|
+
swing_knock: "A brute knocked you down",
|
|
896
|
+
pounce_knock: "A leaper knocked you down",
|
|
897
|
+
migration: "A crowd is on the move",
|
|
898
|
+
door_broken: "A door gave way",
|
|
899
|
+
alarm: "A bird raises the alarm",
|
|
900
|
+
bark: "The dog is barking",
|
|
901
|
+
growl: "The dog is growling",
|
|
902
|
+
bitten: "A civilian was bitten",
|
|
903
|
+
turned: "A civilian turned",
|
|
904
|
+
died: "A civilian died"
|
|
905
|
+
},
|
|
906
|
+
yes: "yes",
|
|
907
|
+
no: "no"
|
|
908
|
+
};
|
|
909
|
+
var PT_BR = {
|
|
910
|
+
group: "Multid\xE3o",
|
|
911
|
+
name: "\xC1rea da multid\xE3o",
|
|
912
|
+
mix: "Misturado (pelo peso)",
|
|
913
|
+
controls: {
|
|
914
|
+
scene: "Cena",
|
|
915
|
+
physics: "F\xEDsica da multid\xE3o",
|
|
916
|
+
count: "Agentes",
|
|
917
|
+
type: "Tipo dos novos",
|
|
918
|
+
night: "Noite",
|
|
919
|
+
seed: "Semente",
|
|
920
|
+
spawn: "Mais 100",
|
|
921
|
+
noise: "Barulho aqui (80 m)",
|
|
922
|
+
hit: "Tiro na cabe\xE7a do mais perto",
|
|
923
|
+
kill_all: "Matar todos",
|
|
924
|
+
join: "Civis v\xEAm com voc\xEA",
|
|
925
|
+
flee: "Civis fogem",
|
|
926
|
+
herd: "Trazer os bichos",
|
|
927
|
+
materialize: "Trazer 50 da regi\xE3o",
|
|
928
|
+
push: "Empurrar (soltar-se)",
|
|
929
|
+
reset: "Recome\xE7ar"
|
|
930
|
+
},
|
|
931
|
+
hints: {
|
|
932
|
+
scene: "Onde a multid\xE3o nasce: o anel aberto, uma horda espremida contra um muro e uma porta, ou uma horda descendo uma rua em dire\xE7\xE3o a uma fileira de botij\xF5es e um carro. Vale antes da f\xEDsica e da contagem.",
|
|
933
|
+
physics: "Contato, press\xE3o, quedas, montes, corpos arremessados e explos\xF5es (a multid\xE3o \xE9 refeita com elas).",
|
|
934
|
+
count: "A multid\xE3o cresce ou diminui at\xE9 esse n\xFAmero, nascendo de 10 a 90 m do centro da \xE1rea. S\xF3 os de perto pensam todo quadro (faixas de relev\xE2ncia).",
|
|
935
|
+
type: "O tipo dos agentes acrescentados daqui em diante; misturado sorteia pelos pesos do pacote.",
|
|
936
|
+
night: "\xC0 noite a vis\xE3o cai 40%.",
|
|
937
|
+
noise: "Um barulho ouvido a 80 m de voc\xEA: quem ouve vem.",
|
|
938
|
+
hit: "Um tiro na cabe\xE7a do agente mais perto: um corpo no ch\xE3o, sangue pelos efeitos da sala.",
|
|
939
|
+
join: "Os civis seguem voc\xEA; mordidos, viram depois do tempo da mordida.",
|
|
940
|
+
flee: "Os civis fogem por um caminho para fora da \xE1rea e somem.",
|
|
941
|
+
herd: "Um c\xE3o companheiro, galinhas, um quero-quero, vacas, um cavalo, um porco, uma capivara, uma carca\xE7a e os urubus sobre ela.",
|
|
942
|
+
materialize: "O modelo da regi\xE3o transforma 50 dos seus n\xFAmeros em agentes perto de voc\xEA; os de longe voltam.",
|
|
943
|
+
push: "Quem segura voc\xEA solta depois de seis empurr\xF5es."
|
|
944
|
+
},
|
|
945
|
+
layers: { corpses: "Corpos no ch\xE3o", civilians: "Civis", animals: "Bichos" },
|
|
946
|
+
metrics: {
|
|
947
|
+
agents: "Agentes",
|
|
948
|
+
drawn: "Desenhados",
|
|
949
|
+
lod_near: "N\xEDvel perto",
|
|
950
|
+
lod_middle: "N\xEDvel meio",
|
|
951
|
+
lod_far: "N\xEDvel longe",
|
|
952
|
+
draw_calls: "Chamadas de desenho",
|
|
953
|
+
bake_ms: "Cozimento",
|
|
954
|
+
detail_scale: "Or\xE7amento de detalhe",
|
|
955
|
+
crowd_gpu_ms: "Tempo de GPU da horda",
|
|
956
|
+
active: "Pensando todo quadro",
|
|
957
|
+
kills: "Mortes",
|
|
958
|
+
corpses: "Corpos",
|
|
959
|
+
civilians: "Civis vivos",
|
|
960
|
+
health: "Sua vida",
|
|
961
|
+
grabbed: "Agarrado",
|
|
962
|
+
message: "\xDAltima mensagem",
|
|
963
|
+
falls_pressure: "Quedas por press\xE3o",
|
|
964
|
+
falls_chain: "Quedas em cadeia al\xE9m da explos\xE3o",
|
|
965
|
+
falls_landing: "Quedas por corpo que pousa",
|
|
966
|
+
climbers: "No monte, pico",
|
|
967
|
+
mound_max: "Maior monte, pico",
|
|
968
|
+
thrown_peak: "Arremessados, pico",
|
|
969
|
+
max_overlap: "Maior sobreposi\xE7\xE3o, pico",
|
|
970
|
+
chain_links: "Elos da cadeia",
|
|
971
|
+
crowd_ms_p99: "Passo da multid\xE3o, p99"
|
|
972
|
+
},
|
|
973
|
+
sceneNames: { open: "Anel aberto", dense_wall: "Muro denso", chain_line: "Rua em cadeia" },
|
|
974
|
+
scenarios: {
|
|
975
|
+
relevance: "Relev\xE2ncia: 500 agentes, s\xF3 os de perto todo quadro",
|
|
976
|
+
lod: "N\xEDvel de detalhe com 1,5 m de histerese",
|
|
977
|
+
attack_pose: "Pose de ataque entra a 1,6 m e sai a 2,2 m",
|
|
978
|
+
corpse_darkens: "O corpo escurece e encolhe no fim da vida",
|
|
979
|
+
civilians_flee: "Civis seguem, depois fogem e somem",
|
|
980
|
+
herd_calls: "A galinha foge e cacareja, o quero-quero d\xE1 o alarme",
|
|
981
|
+
vultures: "Os urubus circulam os corpos",
|
|
982
|
+
infection: "N\xFAmeros da regi\xE3o viram agentes e voltam",
|
|
983
|
+
determinism: "A mesma semente, a mesma multid\xE3o"
|
|
984
|
+
},
|
|
985
|
+
messages: {
|
|
986
|
+
called: "Um gritador chamou a multid\xE3o",
|
|
987
|
+
burst_hit: "Atingido pelo estouro de um inchado",
|
|
988
|
+
swing_knock: "Um brutamonte derrubou voc\xEA",
|
|
989
|
+
pounce_knock: "Um saltador derrubou voc\xEA",
|
|
990
|
+
migration: "Uma multid\xE3o est\xE1 migrando",
|
|
991
|
+
door_broken: "Uma porta cedeu",
|
|
992
|
+
alarm: "Um p\xE1ssaro d\xE1 o alarme",
|
|
993
|
+
bark: "O c\xE3o est\xE1 latindo",
|
|
994
|
+
growl: "O c\xE3o est\xE1 rosnando",
|
|
995
|
+
bitten: "Um civil foi mordido",
|
|
996
|
+
turned: "Um civil virou",
|
|
997
|
+
died: "Um civil morreu"
|
|
998
|
+
},
|
|
999
|
+
yes: "sim",
|
|
1000
|
+
no: "n\xE3o"
|
|
1001
|
+
};
|
|
1002
|
+
var TEXT = { en: EN, "pt-BR": PT_BR };
|
|
1003
|
+
var crowdBenchTextOf = (locale) => TEXT[locale] ?? PT_BR;
|
|
1004
|
+
|
|
1005
|
+
// src/bench/scenes.ts
|
|
1006
|
+
import { STATE as STATE2, CELL } from "@adatechnology/game-crowd";
|
|
1007
|
+
import { discardObject3D } from "@adatechnology/game-shapes-three";
|
|
1008
|
+
var WALL_LENGTH = 30;
|
|
1009
|
+
var WALL_HEIGHT = 3;
|
|
1010
|
+
var WALL_THICKNESS = 0.5;
|
|
1011
|
+
var DOOR_WIDTH = 1.2;
|
|
1012
|
+
var BODY_COLUMNS = 50;
|
|
1013
|
+
var COLUMN_GAP = 0.6;
|
|
1014
|
+
var ROW_GAP = 0.55;
|
|
1015
|
+
var STREET_HALF_WIDTH = 6;
|
|
1016
|
+
var STREET_COLUMNS = 18;
|
|
1017
|
+
var STREET_LENGTH = 90;
|
|
1018
|
+
var CYLINDERS = 6;
|
|
1019
|
+
var CYLINDER_GAP = 1.5;
|
|
1020
|
+
var bear = (crowd, x, z, type) => {
|
|
1021
|
+
const i = crowd.spawn(x, z, type);
|
|
1022
|
+
if (i >= 0) crowd.store.state[i] = STATE2.CHASE;
|
|
1023
|
+
};
|
|
1024
|
+
var removers = (list) => () => {
|
|
1025
|
+
for (const remove of list) remove();
|
|
1026
|
+
};
|
|
1027
|
+
var DENSE_WALL = {
|
|
1028
|
+
id: "dense_wall",
|
|
1029
|
+
layout(crowd, { cx, cz }) {
|
|
1030
|
+
const half = WALL_LENGTH / 2;
|
|
1031
|
+
crowd.grid.fillRect(cx - half, cz - WALL_THICKNESS / 2, cx - DOOR_WIDTH / 2, cz + WALL_THICKNESS / 2, CELL.SOLID, WALL_HEIGHT);
|
|
1032
|
+
crowd.grid.fillRect(cx + DOOR_WIDTH / 2, cz - WALL_THICKNESS / 2, cx + half, cz + WALL_THICKNESS / 2, CELL.SOLID, WALL_HEIGHT);
|
|
1033
|
+
crowd.doors.add(cx, cz, "x", DOOR_WIDTH);
|
|
1034
|
+
},
|
|
1035
|
+
spawn(crowd, { first, count, type }, { cx, cz }) {
|
|
1036
|
+
for (let k = first; k < first + count; k++) {
|
|
1037
|
+
const row = Math.floor(k / BODY_COLUMNS);
|
|
1038
|
+
bear(crowd, cx + (k % BODY_COLUMNS - BODY_COLUMNS / 2 + row % 2 * 0.5) * COLUMN_GAP, cz - 1.5 - row * ROW_GAP, type);
|
|
1039
|
+
}
|
|
1040
|
+
},
|
|
1041
|
+
context: ({ cx, cz }) => ({ bandCentre: [cx, cz - 8] }),
|
|
1042
|
+
view: ({ cx, cz }) => ({ eye: [cx, 1.7, cz + 9], target: [cx, 1.2, cz - 10] }),
|
|
1043
|
+
props(host, { cx, cz }, props) {
|
|
1044
|
+
const three = host.THREE;
|
|
1045
|
+
const side = (WALL_LENGTH - DOOR_WIDTH) / 2;
|
|
1046
|
+
const made = [];
|
|
1047
|
+
for (const sign of [-1, 1]) {
|
|
1048
|
+
const wall = props.wall?.(three, { length: side, height: WALL_HEIGHT });
|
|
1049
|
+
if (!wall) continue;
|
|
1050
|
+
wall.position.set(cx + sign * (DOOR_WIDTH / 2 + side / 2), 0, cz);
|
|
1051
|
+
host.scene.add(wall);
|
|
1052
|
+
made.push(wall);
|
|
1053
|
+
}
|
|
1054
|
+
return () => {
|
|
1055
|
+
discardObject3D(made);
|
|
1056
|
+
};
|
|
1057
|
+
}
|
|
1058
|
+
};
|
|
1059
|
+
var CHAIN_LINE = {
|
|
1060
|
+
id: "chain_line",
|
|
1061
|
+
layout(crowd, { cx, cz }) {
|
|
1062
|
+
for (const sign of [-1, 1]) crowd.grid.fillRect(cx + sign * STREET_HALF_WIDTH, cz - STREET_LENGTH, cx + sign * (STREET_HALF_WIDTH + WALL_THICKNESS), cz + 20, CELL.SOLID, WALL_HEIGHT);
|
|
1063
|
+
},
|
|
1064
|
+
spawn(crowd, { first, count, type }, { cx, cz }) {
|
|
1065
|
+
for (let k = first; k < first + count; k++) {
|
|
1066
|
+
const row = Math.floor(k / STREET_COLUMNS);
|
|
1067
|
+
bear(crowd, cx + (k % STREET_COLUMNS - STREET_COLUMNS / 2 + row % 2 * 0.5) * COLUMN_GAP, cz - 10 - row * ROW_GAP, type);
|
|
1068
|
+
}
|
|
1069
|
+
},
|
|
1070
|
+
context: ({ cx, cz }) => ({ bandCentre: [cx, cz - 14] }),
|
|
1071
|
+
view: ({ cx, cz }) => ({ eye: [cx, 1.7, cz + 14], target: [cx, 1.2, cz - 10] }),
|
|
1072
|
+
props(host, { cx, cz }, props) {
|
|
1073
|
+
const items = props.items;
|
|
1074
|
+
const placed = [];
|
|
1075
|
+
if (!host.addObject || !items) return removers(placed);
|
|
1076
|
+
if (items.gasCylinder) {
|
|
1077
|
+
for (let k = 0; k < CYLINDERS; k++) {
|
|
1078
|
+
const object = host.addObject(items.gasCylinder, { position: [cx + (k - (CYLINDERS - 1) / 2) * CYLINDER_GAP, 0, cz - 4] });
|
|
1079
|
+
if (object) placed.push(() => object.remove());
|
|
1080
|
+
}
|
|
1081
|
+
}
|
|
1082
|
+
for (const id of [items.car, items.carTank]) {
|
|
1083
|
+
const object = id ? host.addObject(id, { position: [cx, 0, cz + 2], rotationY: Math.PI / 2 }) : null;
|
|
1084
|
+
if (object) placed.push(() => object.remove());
|
|
1085
|
+
}
|
|
1086
|
+
return removers(placed);
|
|
1087
|
+
}
|
|
1088
|
+
};
|
|
1089
|
+
var BUILTIN_SCENES = { dense_wall: DENSE_WALL, chain_line: CHAIN_LINE };
|
|
1090
|
+
|
|
1091
|
+
// src/bench/module.ts
|
|
1092
|
+
var CROWD_AREA_ID = "crowd_area";
|
|
1093
|
+
var CROWD_EFFECTS = {
|
|
1094
|
+
blood: { effect: "blood_hit" },
|
|
1095
|
+
limb: { effect: "dismember" },
|
|
1096
|
+
cloud: { effect: "bloater_cloud" },
|
|
1097
|
+
spark: { effect: "impact", param: "metal" },
|
|
1098
|
+
dust: { effect: "impact", param: "concrete" }
|
|
1099
|
+
};
|
|
1100
|
+
var CAPACITY = 4e3;
|
|
1101
|
+
var FRAME = 1 / 30;
|
|
1102
|
+
var STEP_WINDOW = 600;
|
|
1103
|
+
var LETHAL_SHARE = 1 / 3;
|
|
1104
|
+
var HORDE_BUDGET_LEVELS = [160, 320, 600];
|
|
1105
|
+
var RING = [10, 90];
|
|
1106
|
+
var LOTS = { home: { x: -14, z: -14, w: 8, d: 10, house: true, front: 1 }, shop: { x: 8, z: -14, w: 10, d: 10, front: 1 } };
|
|
1107
|
+
var CIVILIANS = [
|
|
1108
|
+
{ id: "civilian_1", home: "home", color: 4090506 },
|
|
1109
|
+
{ id: "civilian_2", home: "home", color: 13672496, child: true },
|
|
1110
|
+
{ id: "civilian_3", home: "shop", color: 6969914 },
|
|
1111
|
+
{ id: "civilian_4", home: "shop", color: 9058906 }
|
|
1112
|
+
];
|
|
1113
|
+
var WAY_OUT = [
|
|
1114
|
+
[0, 4],
|
|
1115
|
+
[30, 4],
|
|
1116
|
+
[60, 20]
|
|
1117
|
+
];
|
|
1118
|
+
var FARM = [
|
|
1119
|
+
["dog", 2, 3, { companion: true }],
|
|
1120
|
+
["cat", 12, -6],
|
|
1121
|
+
["chicken", -8, 4],
|
|
1122
|
+
["chicken", -8.5, 5],
|
|
1123
|
+
["chicken", -7.5, 4.6],
|
|
1124
|
+
["southern_lapwing", 8, 5],
|
|
1125
|
+
["cow", -20, -10],
|
|
1126
|
+
["cow", -24, -12],
|
|
1127
|
+
["horse", -26, -4],
|
|
1128
|
+
["pig", -14, -16],
|
|
1129
|
+
["capybara", 18, 14],
|
|
1130
|
+
["cow", -18, -24, { dead: true }],
|
|
1131
|
+
["vulture", 0, 0],
|
|
1132
|
+
["vulture", 0, 0],
|
|
1133
|
+
["vulture", 0, 0]
|
|
1134
|
+
];
|
|
1135
|
+
var messageText = (text, id) => text.messages[id] ?? id;
|
|
1136
|
+
function createCrowdTestModule(options) {
|
|
1137
|
+
const locale = options.locale ?? "pt-BR";
|
|
1138
|
+
const text = crowdBenchTextOf(locale);
|
|
1139
|
+
const effects = options.effects ?? CROWD_EFFECTS;
|
|
1140
|
+
const [cx, cz] = options.center ?? [0, -45];
|
|
1141
|
+
const settings = { count: options.startCount ?? 0, type: "mix", night: false, seed: options.seed ?? 1, scene: "open", physics: options.crowdPhysics !== void 0 };
|
|
1142
|
+
const scenes = { ...BUILTIN_SCENES, ...options.scenes };
|
|
1143
|
+
const sceneOf = () => settings.scene === "open" ? void 0 : scenes[settings.scene];
|
|
1144
|
+
const layers = { corpses: true, civilians: true, animals: true };
|
|
1145
|
+
let host = null;
|
|
1146
|
+
let three = null;
|
|
1147
|
+
let renderer = null;
|
|
1148
|
+
let herdView = null;
|
|
1149
|
+
let lastMessage = "";
|
|
1150
|
+
let isDirty = false;
|
|
1151
|
+
let removeProps = null;
|
|
1152
|
+
const stepTimes = [];
|
|
1153
|
+
const peaks = { climbers: 0, moundMax: 0, maxOverlap: 0 };
|
|
1154
|
+
let isBaked = false;
|
|
1155
|
+
let frameStepMs = 0;
|
|
1156
|
+
let pending = {};
|
|
1157
|
+
let last = null;
|
|
1158
|
+
const offs = [];
|
|
1159
|
+
const typeOfSetting = (crowd) => settings.type === "mix" ? -1 : crowd.typeIndex(settings.type);
|
|
1160
|
+
const build = (seed) => {
|
|
1161
|
+
const player = createPlayer({ seed });
|
|
1162
|
+
const physics = settings.physics ? options.crowdPhysics ?? CROWD_PHYSICS_DEFAULTS : void 0;
|
|
1163
|
+
const crowd = createCrowd({ pack: options.pack, seed, capacity: CAPACITY, target: player, ...physics ? { physics } : {} });
|
|
1164
|
+
sceneOf()?.layout(crowd, { cx, cz });
|
|
1165
|
+
const civilians = createCivilians(crowd, { people: CIVILIANS.map((c) => ({ ...c })), buildings: LOTS });
|
|
1166
|
+
const herd = createHerd(crowd, { view: () => ({ x: player.state.x, z: player.state.z }) });
|
|
1167
|
+
const flock = createFlock({ count: 0, mode: "carrion" });
|
|
1168
|
+
const model = options.infection?.create() ?? null;
|
|
1169
|
+
const spot = createRng(seed + 7);
|
|
1170
|
+
const binding = model ? bindInfection(crowd, model, { place: () => [cx + (spot() - 0.5) * 40, cz - 20 - spot() * 40], civilians, regionOf: () => options.infection.region }) : null;
|
|
1171
|
+
const place = () => {
|
|
1172
|
+
const a = spot() * Math.PI * 2;
|
|
1173
|
+
const r = RING[0] + spot() * (RING[1] - RING[0]);
|
|
1174
|
+
const type = typeOfSetting(crowd);
|
|
1175
|
+
return crowd.spawn(cx + Math.cos(a) * r, cz + Math.sin(a) * r, type);
|
|
1176
|
+
};
|
|
1177
|
+
return { crowd, player, civilians, herd, flock, vultures: 0, binding, model, place, physics: physics !== void 0, sceneUnit: createRng(seed + 13) };
|
|
1178
|
+
};
|
|
1179
|
+
let world = build(settings.seed);
|
|
1180
|
+
const wire = () => {
|
|
1181
|
+
for (const off of offs.splice(0)) off();
|
|
1182
|
+
const { crowd, herd, civilians } = world;
|
|
1183
|
+
offs.push(crowd.on("sound", (e) => void host?.audio?.play({ sound: e.sound, volume: e.volume, position: e.position })));
|
|
1184
|
+
offs.push(herd.on("sound", (e) => void host?.audio?.play({ sound: HERD_SOUND_IDS[e.sound] ?? e.sound, volume: e.volume, position: e.position })));
|
|
1185
|
+
offs.push(crowd.on("effect", (e) => playEffect(e)));
|
|
1186
|
+
offs.push(crowd.on("message", (e) => void (lastMessage = messageText(text, e.id))));
|
|
1187
|
+
offs.push(herd.on("message", (e) => void (lastMessage = messageText(text, e.id))));
|
|
1188
|
+
offs.push(civilians.on("bitten", () => void (lastMessage = text.messages.bitten)));
|
|
1189
|
+
offs.push(civilians.on("turned", () => void (lastMessage = text.messages.turned)));
|
|
1190
|
+
offs.push(civilians.on("died", () => void (lastMessage = text.messages.died)));
|
|
1191
|
+
offs.push(crowd.on("kill", (e) => host?.log(`crowd kill: ${e.type}`, { agent: e.agent, headshot: e.headshot })));
|
|
1192
|
+
};
|
|
1193
|
+
const playEffect = (e) => {
|
|
1194
|
+
const map = effects[e.effect];
|
|
1195
|
+
if (!map || !host?.effects) return;
|
|
1196
|
+
host.effects.play({ effect: map.effect, point: e.point, ...map.param !== void 0 ? { param: map.param } : {}, ...e.amount !== void 0 ? { scale: Math.max(0.3, Math.min(2, e.amount / 30)) } : {} });
|
|
1197
|
+
};
|
|
1198
|
+
const fit = () => {
|
|
1199
|
+
settle();
|
|
1200
|
+
const { crowd } = world;
|
|
1201
|
+
const scene = sceneOf();
|
|
1202
|
+
if (scene) {
|
|
1203
|
+
const missing = Math.min(settings.count, CAPACITY) - crowd.store.n;
|
|
1204
|
+
if (missing > 0) scene.spawn(crowd, { first: crowd.store.n, count: missing, type: typeOfSetting(crowd), unit: world.sceneUnit }, { cx, cz });
|
|
1205
|
+
} else {
|
|
1206
|
+
while (crowd.store.n < settings.count && world.place() >= 0) {
|
|
1207
|
+
}
|
|
1208
|
+
}
|
|
1209
|
+
while (crowd.store.n > settings.count) crowd.remove(crowd.store.n - 1);
|
|
1210
|
+
};
|
|
1211
|
+
const bringHerd = () => {
|
|
1212
|
+
const animals = options.animals;
|
|
1213
|
+
if (!animals || world.herd.list.length) return;
|
|
1214
|
+
for (const [id, dx, dz, extra] of FARM) {
|
|
1215
|
+
const species = animals.species(id);
|
|
1216
|
+
if (!species) continue;
|
|
1217
|
+
world.herd.add({ ...HERD_ROLES[id], ...extra, species: id, x: cx + dx, z: cz + 30 + dz, walkHz: species.walkHz, runHz: species.runHz, size: animals.size(species) });
|
|
1218
|
+
}
|
|
1219
|
+
world.vultures = world.herd.list.filter((E) => E.spec.sky).length;
|
|
1220
|
+
world.flock = createFlock({ count: world.vultures, mode: "carrion" });
|
|
1221
|
+
herdView?.clear();
|
|
1222
|
+
herdView = null;
|
|
1223
|
+
};
|
|
1224
|
+
const sceneView = () => {
|
|
1225
|
+
const view2 = sceneOf()?.view({ cx, cz });
|
|
1226
|
+
return view2 ? { eye: [...view2.eye], target: [...view2.target] } : void 0;
|
|
1227
|
+
};
|
|
1228
|
+
const settle = () => {
|
|
1229
|
+
if (isDirty) reset();
|
|
1230
|
+
};
|
|
1231
|
+
const reset = () => {
|
|
1232
|
+
isDirty = false;
|
|
1233
|
+
stepTimes.length = 0;
|
|
1234
|
+
peaks.climbers = peaks.moundMax = peaks.maxOverlap = 0;
|
|
1235
|
+
removeProps?.();
|
|
1236
|
+
removeProps = null;
|
|
1237
|
+
world.binding?.unbind();
|
|
1238
|
+
renderer?.dispose();
|
|
1239
|
+
if (renderer) host?.scene?.remove(renderer.group);
|
|
1240
|
+
renderer = null;
|
|
1241
|
+
herdView?.clear();
|
|
1242
|
+
if (herdView) host?.scene?.remove(herdView.group);
|
|
1243
|
+
herdView = null;
|
|
1244
|
+
world = build(settings.seed);
|
|
1245
|
+
wire();
|
|
1246
|
+
if (host) removeProps = sceneOf()?.props?.(host, { cx, cz }, options.props ?? {}) ?? null;
|
|
1247
|
+
fit();
|
|
1248
|
+
lastMessage = "";
|
|
1249
|
+
const view2 = sceneView();
|
|
1250
|
+
if (view2) host?.bench?.look?.(view2.eye, view2.target);
|
|
1251
|
+
};
|
|
1252
|
+
const view = () => {
|
|
1253
|
+
const camera = host?.camera;
|
|
1254
|
+
if (!camera?.position) return null;
|
|
1255
|
+
const Vector3 = host?.THREE?.Vector3;
|
|
1256
|
+
if (camera.getWorldDirection && Vector3) {
|
|
1257
|
+
const look = camera.getWorldDirection(new Vector3());
|
|
1258
|
+
return { x: camera.position.x, z: camera.position.z, yaw: Math.atan2(-look.x, -look.z) };
|
|
1259
|
+
}
|
|
1260
|
+
return { x: camera.position.x, z: camera.position.z, yaw: camera.rotation?.y ?? 0 };
|
|
1261
|
+
};
|
|
1262
|
+
const varietyFor = (detail) => {
|
|
1263
|
+
const variety = options.variety;
|
|
1264
|
+
if (!variety || detail === "auto" || variety.budget === false) return variety;
|
|
1265
|
+
const levels = (variety.budget?.levels ?? HORDE_BUDGET_LEVELS).map((n) => Math.max(1, Math.round(n * detail)));
|
|
1266
|
+
return { ...variety, budget: { ...variety.budget, levels, adapt: false } };
|
|
1267
|
+
};
|
|
1268
|
+
const ensureDrawing = () => {
|
|
1269
|
+
if (!host || !three) return;
|
|
1270
|
+
const scene = host.scene;
|
|
1271
|
+
if (!renderer && options.builders) {
|
|
1272
|
+
const bodies = options.civilianBodies ?? [];
|
|
1273
|
+
const variety = varietyFor(host.graphics?.hordeDetail() ?? "auto");
|
|
1274
|
+
renderer = createCrowdRenderer(three, {
|
|
1275
|
+
crowd: world.crowd,
|
|
1276
|
+
builders: options.builders(three),
|
|
1277
|
+
...options.typeName ? { typeName: options.typeName } : {},
|
|
1278
|
+
drawCorpses: () => layers.corpses,
|
|
1279
|
+
...variety ? { variety } : {},
|
|
1280
|
+
...bodies.length ? { civilians: { list: () => layers.civilians ? world.civilians.list : [], body: (n) => bodies[n.k % bodies.length] } } : {}
|
|
1281
|
+
});
|
|
1282
|
+
scene.add(renderer.group);
|
|
1283
|
+
isBaked = false;
|
|
1284
|
+
}
|
|
1285
|
+
if (!herdView && options.animals?.drawer && world.herd.list.length) {
|
|
1286
|
+
herdView = createHerdView(three, { herd: world.herd, flock: world.flock, drawer: options.animals.drawer(three), species: options.animals.species });
|
|
1287
|
+
scene.add(herdView.group);
|
|
1288
|
+
}
|
|
1289
|
+
};
|
|
1290
|
+
const updateContext = () => ({
|
|
1291
|
+
viewYaw: view()?.yaw ?? 0,
|
|
1292
|
+
nightFactor: settings.night ? 1 : 0,
|
|
1293
|
+
...sceneOf()?.context?.({ cx, cz })
|
|
1294
|
+
});
|
|
1295
|
+
const stepCrowd = (dt) => {
|
|
1296
|
+
const started = performance.now();
|
|
1297
|
+
world.crowd.update(dt, updateContext());
|
|
1298
|
+
frameStepMs += performance.now() - started;
|
|
1299
|
+
};
|
|
1300
|
+
const recordPeaks = () => {
|
|
1301
|
+
const physics = world.crowd.stats.physics;
|
|
1302
|
+
peaks.climbers = Math.max(peaks.climbers, physics.climbers);
|
|
1303
|
+
peaks.moundMax = Math.max(peaks.moundMax, physics.moundMax);
|
|
1304
|
+
peaks.maxOverlap = Math.max(peaks.maxOverlap, physics.maxOverlap);
|
|
1305
|
+
if (frameStepMs <= 0) return;
|
|
1306
|
+
stepTimes.push(frameStepMs);
|
|
1307
|
+
if (stepTimes.length > STEP_WINDOW) stepTimes.shift();
|
|
1308
|
+
frameStepMs = 0;
|
|
1309
|
+
};
|
|
1310
|
+
const stepMsP99 = () => {
|
|
1311
|
+
if (stepTimes.length === 0) return 0;
|
|
1312
|
+
const sorted = [...stepTimes].sort((a, b) => a - b);
|
|
1313
|
+
return sorted[Math.min(sorted.length - 1, Math.floor(sorted.length * 0.99))];
|
|
1314
|
+
};
|
|
1315
|
+
const prepareDrawing = () => {
|
|
1316
|
+
if (!host) return;
|
|
1317
|
+
settle();
|
|
1318
|
+
ensureDrawing();
|
|
1319
|
+
if (!renderer || isBaked) return;
|
|
1320
|
+
renderer.prepare();
|
|
1321
|
+
isBaked = true;
|
|
1322
|
+
};
|
|
1323
|
+
const frame = (dt) => {
|
|
1324
|
+
settle();
|
|
1325
|
+
const { crowd, player, civilians, herd, flock } = world;
|
|
1326
|
+
const here = view();
|
|
1327
|
+
const speed = here && last && dt > 0 ? Math.hypot(here.x - last.x, here.z - last.z) / dt : 0;
|
|
1328
|
+
if (here) {
|
|
1329
|
+
player.state.x = here.x;
|
|
1330
|
+
player.state.z = here.z;
|
|
1331
|
+
}
|
|
1332
|
+
player.update(dt, { forward: speed > 0.2 ? 1 : 0, run: speed > 3.4, yaw: here?.yaw ?? 0, pressed: pending }, { crowd: crowd.asPlayerCrowd() });
|
|
1333
|
+
pending = {};
|
|
1334
|
+
if (here) {
|
|
1335
|
+
player.state.x = here.x;
|
|
1336
|
+
player.state.z = here.z;
|
|
1337
|
+
last = { x: here.x, z: here.z };
|
|
1338
|
+
}
|
|
1339
|
+
crowd.updateTrail(dt);
|
|
1340
|
+
civilians.update(dt);
|
|
1341
|
+
if (herd.list.length) {
|
|
1342
|
+
herd.update(dt);
|
|
1343
|
+
const carcass = herd.list.find((E) => E.dead);
|
|
1344
|
+
flock.update(herd.time, dt, carrionCenter(crowd.corpses.entries.map((e) => e.data), carcass ? [carcass.x, carcass.z] : null), [player.state.x, player.state.z]);
|
|
1345
|
+
}
|
|
1346
|
+
if (!world.physics) stepCrowd(dt);
|
|
1347
|
+
recordPeaks();
|
|
1348
|
+
crowd.updateCorpses(dt);
|
|
1349
|
+
if (options.eager) prepareDrawing();
|
|
1350
|
+
else ensureDrawing();
|
|
1351
|
+
if (renderer && (crowd.store.n || crowd.corpses.count || civilians.list.length)) {
|
|
1352
|
+
renderer.update(dt, { camera: { x: player.state.x, z: player.state.z }, viewYaw: here?.yaw ?? 0, target: { x: player.state.x, z: player.state.z, alive: player.state.alive } });
|
|
1353
|
+
const detail = host?.graphics?.hordeDetail() ?? "auto";
|
|
1354
|
+
host?.graphics?.report("hordeDetail", detail === "auto" ? renderer.detailScale : detail);
|
|
1355
|
+
}
|
|
1356
|
+
if (herdView) {
|
|
1357
|
+
herdView.group.visible = layers.animals;
|
|
1358
|
+
herdView.update(dt);
|
|
1359
|
+
}
|
|
1360
|
+
};
|
|
1361
|
+
const hitNearest = () => {
|
|
1362
|
+
const { crowd, player } = world;
|
|
1363
|
+
const Z = crowd.store;
|
|
1364
|
+
let best = -1;
|
|
1365
|
+
let bd = Infinity;
|
|
1366
|
+
for (let i = 0; i < Z.n; i++) {
|
|
1367
|
+
const d = Math.hypot(Z.x[i] - player.state.x, Z.z[i] - player.state.z);
|
|
1368
|
+
if (d < bd) [best, bd] = [i, d];
|
|
1369
|
+
}
|
|
1370
|
+
if (best >= 0) crowd.damage(best, { region: 0, amount: 200, distance: bd, cut: 0, caliber: null });
|
|
1371
|
+
};
|
|
1372
|
+
const onChange = (change) => {
|
|
1373
|
+
if (change.kind === "layer") {
|
|
1374
|
+
if (change.id in layers) layers[change.id] = change.on;
|
|
1375
|
+
return;
|
|
1376
|
+
}
|
|
1377
|
+
if (change.kind === "action") {
|
|
1378
|
+
const id2 = change.id;
|
|
1379
|
+
if (id2 === "spawn") {
|
|
1380
|
+
settings.count = Math.min(CAPACITY, world.crowd.store.n + 100);
|
|
1381
|
+
host?.bench?.patch("count", { value: settings.count });
|
|
1382
|
+
fit();
|
|
1383
|
+
} else if (id2 === "noise") world.crowd.emitNoise(world.player.state.x, world.player.state.z, 80);
|
|
1384
|
+
else if (id2 === "hit") hitNearest();
|
|
1385
|
+
else if (id2 === "kill_all") for (let i = world.crowd.store.n - 1; i >= 0; i--) world.crowd.kill(i);
|
|
1386
|
+
else if (id2 === "join") for (const n of world.civilians.list) world.civilians.join(n.id);
|
|
1387
|
+
else if (id2 === "flee") for (const n of world.civilians.list) world.civilians.flee(n.id, WAY_OUT.map(([x, z]) => [cx + x, cz + 40 + z]));
|
|
1388
|
+
else if (id2 === "herd") bringHerd();
|
|
1389
|
+
else if (id2 === "materialize" && world.binding && options.infection) world.binding.materialize(options.infection.region, 50);
|
|
1390
|
+
else if (id2 === "push") pending = { ...pending, push: true };
|
|
1391
|
+
else if (id2 === "reset") reset();
|
|
1392
|
+
return;
|
|
1393
|
+
}
|
|
1394
|
+
const { id, value } = change;
|
|
1395
|
+
if (id === "count" && typeof value === "number") {
|
|
1396
|
+
settings.count = value;
|
|
1397
|
+
fit();
|
|
1398
|
+
} else if (id === "scene" && typeof value === "string" && value !== settings.scene) {
|
|
1399
|
+
settings.scene = value;
|
|
1400
|
+
isDirty = true;
|
|
1401
|
+
} else if (id === "physics" && typeof value === "boolean" && value !== settings.physics) {
|
|
1402
|
+
settings.physics = value;
|
|
1403
|
+
isDirty = true;
|
|
1404
|
+
} else if (id === "type" && typeof value === "string") settings.type = value;
|
|
1405
|
+
else if (id === "night" && typeof value === "boolean") settings.night = value;
|
|
1406
|
+
else if (id === "seed" && typeof value === "number" && value !== settings.seed) {
|
|
1407
|
+
settings.seed = value;
|
|
1408
|
+
isDirty = true;
|
|
1409
|
+
}
|
|
1410
|
+
};
|
|
1411
|
+
const typeOptions = () => [{ id: "mix", name: text.mix }, ...options.pack.types.map((type) => ({ id: type.id, name: options.typeLabel?.(type, locale) ?? type.id }))];
|
|
1412
|
+
const sceneOptions = () => [{ id: "open", name: text.sceneNames.open }, ...Object.keys(scenes).map((id) => ({ id, name: text.sceneNames[id] ?? id }))];
|
|
1413
|
+
const controls = () => [
|
|
1414
|
+
// the order is the order the bench state is applied in (`sendInitial`): the seed makes the world, the scene writes its grid, the physics rebuilds the crowd, the count bears the agents
|
|
1415
|
+
{ kind: "number", id: "seed", name: text.controls.seed, min: 1, max: 9999, step: 1, default: settings.seed },
|
|
1416
|
+
{ kind: "choice", id: "scene", name: text.controls.scene, hint: text.hints.scene, options: sceneOptions(), default: settings.scene },
|
|
1417
|
+
{ kind: "boolean", id: "physics", name: text.controls.physics, hint: text.hints.physics, default: settings.physics },
|
|
1418
|
+
{ kind: "number", id: "count", name: text.controls.count, hint: text.hints.count, min: 0, max: CAPACITY, step: 100, default: settings.count },
|
|
1419
|
+
{ kind: "choice", id: "type", name: text.controls.type, hint: text.hints.type, options: typeOptions(), default: "mix" },
|
|
1420
|
+
{ kind: "boolean", id: "night", name: text.controls.night, hint: text.hints.night, default: false },
|
|
1421
|
+
{ kind: "action", id: "spawn", name: text.controls.spawn, slow: true },
|
|
1422
|
+
{ kind: "action", id: "noise", name: text.controls.noise, hint: text.hints.noise },
|
|
1423
|
+
{ kind: "action", id: "hit", name: text.controls.hit, hint: text.hints.hit },
|
|
1424
|
+
{ kind: "action", id: "kill_all", name: text.controls.kill_all },
|
|
1425
|
+
{ kind: "action", id: "join", name: text.controls.join, hint: text.hints.join },
|
|
1426
|
+
{ kind: "action", id: "flee", name: text.controls.flee, hint: text.hints.flee },
|
|
1427
|
+
...options.animals ? [{ kind: "action", id: "herd", name: text.controls.herd, hint: text.hints.herd }] : [],
|
|
1428
|
+
...options.infection ? [{ kind: "action", id: "materialize", name: text.controls.materialize, hint: text.hints.materialize }] : [],
|
|
1429
|
+
{ kind: "action", id: "push", name: text.controls.push, hint: text.hints.push },
|
|
1430
|
+
{ kind: "action", id: "reset", name: text.controls.reset, slow: true }
|
|
1431
|
+
];
|
|
1432
|
+
const benchLayers = () => [
|
|
1433
|
+
{ id: "corpses", name: text.layers.corpses },
|
|
1434
|
+
{ id: "civilians", name: text.layers.civilians },
|
|
1435
|
+
{ id: "animals", name: text.layers.animals }
|
|
1436
|
+
];
|
|
1437
|
+
const metrics = () => [
|
|
1438
|
+
{ id: "agents", name: text.metrics.agents, digits: 0, max: CAPACITY, read: () => world.crowd.store.n },
|
|
1439
|
+
{ id: "drawn", name: text.metrics.drawn, digits: 0, read: () => renderer ? renderer.lodCounts[0] + renderer.lodCounts[1] + renderer.lodCounts[2] : 0 },
|
|
1440
|
+
{ id: "lod_near", name: text.metrics.lod_near, digits: 0, read: () => renderer?.lodCounts[0] ?? 0 },
|
|
1441
|
+
{ id: "lod_middle", name: text.metrics.lod_middle, digits: 0, read: () => renderer?.lodCounts[1] ?? 0 },
|
|
1442
|
+
{ id: "lod_far", name: text.metrics.lod_far, digits: 0, read: () => renderer?.lodCounts[2] ?? 0 },
|
|
1443
|
+
{ id: "draw_calls", name: text.metrics.draw_calls, digits: 0, read: () => renderer?.drawCalls ?? 0 },
|
|
1444
|
+
{ id: "bake_ms", name: text.metrics.bake_ms, unit: "ms", digits: 0, read: () => renderer?.bakeMs ?? 0 },
|
|
1445
|
+
{ id: "detail_scale", name: text.metrics.detail_scale, digits: 2, read: () => renderer?.detailScale ?? 1 },
|
|
1446
|
+
{ id: "crowd_gpu_ms", name: text.metrics.crowd_gpu_ms, unit: "ms", digits: 1, read: () => renderer?.gpuMs ?? 0 },
|
|
1447
|
+
{ id: "active", name: text.metrics.active, digits: 0, read: () => world.crowd.stats.bands[0] },
|
|
1448
|
+
{ id: "kills", name: text.metrics.kills, digits: 0, read: () => world.crowd.stats.kills },
|
|
1449
|
+
{ id: "bodies", name: text.metrics.corpses, digits: 0, read: () => world.crowd.corpses.count },
|
|
1450
|
+
{ id: "civilians_alive", name: text.metrics.civilians, digits: 0, read: () => world.civilians.list.filter((n) => n.state !== "dead" && n.state !== "undead" && n.state !== "gone").length },
|
|
1451
|
+
{ id: "health", name: text.metrics.health, digits: 0, read: () => Math.max(0, world.player.state.health) },
|
|
1452
|
+
{ id: "grabbed", name: text.metrics.grabbed, read: () => world.player.state.grabbedBy >= 0 ? text.yes : text.no },
|
|
1453
|
+
{ id: "message", name: text.metrics.message, read: () => lastMessage },
|
|
1454
|
+
{ id: "falls_pressure", name: text.metrics.falls_pressure, digits: 0, read: () => world.crowd.stats.physics.falls[0] },
|
|
1455
|
+
{ id: "falls_chain", name: text.metrics.falls_chain, digits: 0, read: () => world.crowd.stats.physics.fallsBeyondBlast },
|
|
1456
|
+
{ id: "falls_landing", name: text.metrics.falls_landing, digits: 0, read: () => world.crowd.stats.physics.falls[3] },
|
|
1457
|
+
{ id: "climbers", name: text.metrics.climbers, digits: 0, read: () => peaks.climbers },
|
|
1458
|
+
{ id: "mound_max", name: text.metrics.mound_max, unit: "m", digits: 2, read: () => peaks.moundMax },
|
|
1459
|
+
{ id: "thrown_peak", name: text.metrics.thrown_peak, digits: 0, read: () => world.crowd.stats.physics.thrownPeak },
|
|
1460
|
+
{ id: "max_overlap", name: text.metrics.max_overlap, unit: "m", digits: 2, read: () => peaks.maxOverlap },
|
|
1461
|
+
{ id: "chain_links", name: text.metrics.chain_links, digits: 0, read: () => world.crowd.stats.physics.chainLinks },
|
|
1462
|
+
{ id: "crowd_ms_p99", name: text.metrics.crowd_ms_p99, unit: "ms", digits: 2, read: () => stepMsP99() }
|
|
1463
|
+
];
|
|
1464
|
+
const fresh = (seed = 1, capacity = 600) => {
|
|
1465
|
+
const player = createPlayer({ seed });
|
|
1466
|
+
return { crowd: createCrowd({ pack: options.pack, seed, capacity, flow: { clock: () => 0 }, target: player }), player };
|
|
1467
|
+
};
|
|
1468
|
+
const run = (crowd, seconds) => {
|
|
1469
|
+
for (let t = 0; t < seconds; t += FRAME) {
|
|
1470
|
+
crowd.update(FRAME);
|
|
1471
|
+
crowd.updateCorpses(FRAME);
|
|
1472
|
+
}
|
|
1473
|
+
};
|
|
1474
|
+
const eyes = options.pack.types.map((type) => type.eye ?? null);
|
|
1475
|
+
const sizes = Array.from({ length: options.pack.types.reduce((sum, type) => sum + (type.variants ?? 1), 0) }, () => ({ s: 1.7, bulk: 1 }));
|
|
1476
|
+
const drawOnce = (crowd, player) => {
|
|
1477
|
+
const out = [];
|
|
1478
|
+
createCrowdInstances({ eyes }).agents(crowd.store, sizes, { camera: { x: player.state.x, z: player.state.z }, viewYaw: 0, target: { x: player.state.x, z: player.state.z, alive: true } }, (_v, level, instance) => out.push({ level, pose: instance.b[3] }));
|
|
1479
|
+
return out;
|
|
1480
|
+
};
|
|
1481
|
+
const scenarios = () => [
|
|
1482
|
+
{
|
|
1483
|
+
id: "crowd_relevance",
|
|
1484
|
+
name: text.scenarios.relevance,
|
|
1485
|
+
run: ({ expect }) => {
|
|
1486
|
+
const { crowd } = fresh(3);
|
|
1487
|
+
const spot = createRng(3);
|
|
1488
|
+
for (let k = 0; k < 500; k++) crowd.spawn((spot() - 0.5) * 600, (spot() - 0.5) * 600);
|
|
1489
|
+
run(crowd, 1);
|
|
1490
|
+
const [near, , mid, far] = crowd.stats.bands;
|
|
1491
|
+
expect(crowd.store.n === 500, "500 agents", { n: crowd.store.n });
|
|
1492
|
+
expect(near < 50 && mid + far > 400, "few think every frame, most are far and slow", { bands: [...crowd.stats.bands] });
|
|
1493
|
+
}
|
|
1494
|
+
},
|
|
1495
|
+
{
|
|
1496
|
+
id: "crowd_lod",
|
|
1497
|
+
name: text.scenarios.lod,
|
|
1498
|
+
run: ({ expect }) => {
|
|
1499
|
+
const { crowd, player } = fresh(1, 4);
|
|
1500
|
+
player.state.x = 0;
|
|
1501
|
+
player.state.z = 0;
|
|
1502
|
+
const i = crowd.spawn(0, -17, 0);
|
|
1503
|
+
const levels = [];
|
|
1504
|
+
for (const z of [-17, -19, -19.6, -17.5, -16]) {
|
|
1505
|
+
crowd.store.z[i] = z;
|
|
1506
|
+
levels.push(drawOnce(crowd, player)[0].level);
|
|
1507
|
+
}
|
|
1508
|
+
expect(levels.join() === "0,0,1,1,0", "in at 19.5 m, back under 16.5 m", { levels });
|
|
1509
|
+
}
|
|
1510
|
+
},
|
|
1511
|
+
{
|
|
1512
|
+
id: "crowd_attack_pose",
|
|
1513
|
+
name: text.scenarios.attack_pose,
|
|
1514
|
+
run: ({ expect }) => {
|
|
1515
|
+
const { crowd, player } = fresh(1, 4);
|
|
1516
|
+
const i = crowd.spawn(0, -1.5, 0);
|
|
1517
|
+
crowd.store.state[i] = STATE3.CHASE;
|
|
1518
|
+
const poses = [];
|
|
1519
|
+
for (const z of [-1.5, -2, -2.3, -1.9]) {
|
|
1520
|
+
crowd.store.z[i] = z;
|
|
1521
|
+
poses.push(drawOnce(crowd, player)[0].pose);
|
|
1522
|
+
}
|
|
1523
|
+
expect(poses.join() === "3,3,0,0", "the attack pose holds to 2.2 m and comes back under 1.6 m", { poses });
|
|
1524
|
+
}
|
|
1525
|
+
},
|
|
1526
|
+
{
|
|
1527
|
+
id: "crowd_corpse_darkens",
|
|
1528
|
+
name: text.scenarios.corpse_darkens,
|
|
1529
|
+
run: ({ expect }) => {
|
|
1530
|
+
const { crowd, player } = fresh(1, 4);
|
|
1531
|
+
crowd.kill(crowd.spawn(0, -5, 0));
|
|
1532
|
+
const life = crowd.numbers.corpseSeconds;
|
|
1533
|
+
crowd.updateCorpses(life / 2);
|
|
1534
|
+
const shades = [];
|
|
1535
|
+
const heights = [];
|
|
1536
|
+
const take = (_v, _l, instance) => {
|
|
1537
|
+
shades.push(instance.shade);
|
|
1538
|
+
heights.push(instance.b[0]);
|
|
1539
|
+
};
|
|
1540
|
+
const instances = createCrowdInstances({ eyes });
|
|
1541
|
+
const frameOf = { camera: { x: player.state.x, z: player.state.z }, viewYaw: 0 };
|
|
1542
|
+
instances.corpses(crowd.corpses.entries, sizes, frameOf, life, take, (i) => take(-1, 1, i));
|
|
1543
|
+
crowd.updateCorpses(life / 2 - 30);
|
|
1544
|
+
instances.corpses(crowd.corpses.entries, sizes, frameOf, life, take, (i) => take(-1, 1, i));
|
|
1545
|
+
expect(Math.abs(shades[0] - 0.65) < 1e-9, "half its life: 65% of the colour", { shades });
|
|
1546
|
+
expect(Math.abs(heights[1] / heights[0] - 0.5) < 1e-6, "30 s before the end: half its height", { heights });
|
|
1547
|
+
}
|
|
1548
|
+
},
|
|
1549
|
+
{
|
|
1550
|
+
id: "crowd_civilians_flee",
|
|
1551
|
+
name: text.scenarios.civilians_flee,
|
|
1552
|
+
run: ({ expect }) => {
|
|
1553
|
+
const { crowd, player } = fresh(2, 4);
|
|
1554
|
+
player.state.x = 0;
|
|
1555
|
+
player.state.z = 6;
|
|
1556
|
+
const civilians = createCivilians(crowd, { people: [{ id: "c", home: "home" }], buildings: LOTS });
|
|
1557
|
+
civilians.join("c");
|
|
1558
|
+
for (let t = 0; t < 10; t += 0.05) civilians.update(0.05);
|
|
1559
|
+
const c = civilians.get("c");
|
|
1560
|
+
expect(Math.hypot(c.x - 0, c.z - 6) < 2.5, "it follows", { x: c.x, z: c.z });
|
|
1561
|
+
civilians.flee("c", [[5, 6], [10, 6]]);
|
|
1562
|
+
for (let t = 0; t < 20; t += 0.05) civilians.update(0.05);
|
|
1563
|
+
expect(c.state === "gone", "it ran off and is gone", { state: c.state });
|
|
1564
|
+
}
|
|
1565
|
+
},
|
|
1566
|
+
{
|
|
1567
|
+
id: "crowd_herd_calls",
|
|
1568
|
+
name: text.scenarios.herd_calls,
|
|
1569
|
+
run: ({ expect }) => {
|
|
1570
|
+
const { crowd } = fresh(4, 4);
|
|
1571
|
+
const herd = createHerd(crowd);
|
|
1572
|
+
const hen = herd.add({ ...HERD_ROLES.chicken, species: "chicken", x: 20, z: 0, walkHz: 2.2, runHz: 4, size: 0.4 });
|
|
1573
|
+
herd.add({ ...HERD_ROLES.southern_lapwing, species: "southern_lapwing", x: 5, z: 0, walkHz: 2.5, runHz: 1.6, size: 0.35 });
|
|
1574
|
+
crowd.spawn(21.5, 0, 0);
|
|
1575
|
+
const sounds = [];
|
|
1576
|
+
herd.on("sound", (e) => sounds.push(HERD_SOUND_IDS[e.sound] ?? e.sound));
|
|
1577
|
+
for (let f = 0; f < 40; f++) herd.update(0.05);
|
|
1578
|
+
expect(sounds.includes("hen_cluck") && sounds.includes("lapwing_call"), "the hen clucks, the lapwing calls", { sounds });
|
|
1579
|
+
expect(hen.x < 20, "the hen ran", { x: hen.x });
|
|
1580
|
+
}
|
|
1581
|
+
},
|
|
1582
|
+
{
|
|
1583
|
+
id: "crowd_vultures",
|
|
1584
|
+
name: text.scenarios.vultures,
|
|
1585
|
+
run: ({ expect }) => {
|
|
1586
|
+
const centre = carrionCenter([{ x: 0, z: 0 }, { x: 3, z: 0 }, { x: 0, z: 3 }], null);
|
|
1587
|
+
const vultures = createFlock({ count: 3, mode: "carrion" });
|
|
1588
|
+
vultures.update(10, 0.05, centre, [0, 0]);
|
|
1589
|
+
const r = vultures.birds.map((U) => Math.hypot(U.x - centre[0], U.z - centre[1]));
|
|
1590
|
+
expect(r.every((d, i) => Math.abs(d - (7 + i * 2)) < 1e-9), "each at its radius over the centre of the bodies", { r });
|
|
1591
|
+
}
|
|
1592
|
+
},
|
|
1593
|
+
...options.infection ? [
|
|
1594
|
+
{
|
|
1595
|
+
id: "crowd_infection",
|
|
1596
|
+
name: text.scenarios.infection,
|
|
1597
|
+
run: ({ expect }) => {
|
|
1598
|
+
const infection = options.infection;
|
|
1599
|
+
const model = infection.create();
|
|
1600
|
+
const { crowd } = fresh(6, 64);
|
|
1601
|
+
const binding = bindInfection(crowd, model, { place: (_r, seed, k) => [seed % 100 - 50 + k * 0.01, 500 + k] });
|
|
1602
|
+
const before = infection.active?.(model);
|
|
1603
|
+
const born = binding.materialize(infection.region, 10);
|
|
1604
|
+
crowd.update(0.1);
|
|
1605
|
+
const back = binding.giveBack(infection.region);
|
|
1606
|
+
expect(born.length > 0 && back === born.length && crowd.store.n === 0, "brought out, given back", { born: born.length, back });
|
|
1607
|
+
if (before !== void 0) expect(infection.active(model) === before, "the region keeps its numbers", { before, after: infection.active(model) });
|
|
1608
|
+
binding.unbind();
|
|
1609
|
+
}
|
|
1610
|
+
}
|
|
1611
|
+
] : [],
|
|
1612
|
+
{
|
|
1613
|
+
id: "crowd_determinism",
|
|
1614
|
+
name: text.scenarios.determinism,
|
|
1615
|
+
run: ({ expect }) => {
|
|
1616
|
+
const trace = (seed) => {
|
|
1617
|
+
const { crowd } = fresh(seed, 40);
|
|
1618
|
+
for (let k = 0; k < 30; k++) crowd.spawn(k % 6 * 3 - 8, -10 - Math.floor(k / 6) * 3);
|
|
1619
|
+
run(crowd, 3);
|
|
1620
|
+
return Array.from({ length: crowd.store.n }, (_, i) => `${crowd.store.x[i].toFixed(4)},${crowd.store.z[i].toFixed(4)}`).join(";");
|
|
1621
|
+
};
|
|
1622
|
+
expect(trace(9) === trace(9), "the same seed gives the same crowd");
|
|
1623
|
+
expect(trace(9) !== trace(10), "another seed gives another");
|
|
1624
|
+
}
|
|
1625
|
+
}
|
|
1626
|
+
];
|
|
1627
|
+
wire();
|
|
1628
|
+
fit();
|
|
1629
|
+
const module = {
|
|
1630
|
+
id: CROWD_AREA_ID,
|
|
1631
|
+
name: text.name,
|
|
1632
|
+
ground: { surface: "road" },
|
|
1633
|
+
group: text.group,
|
|
1634
|
+
get crowd() {
|
|
1635
|
+
return world.crowd;
|
|
1636
|
+
},
|
|
1637
|
+
get player() {
|
|
1638
|
+
return world.player;
|
|
1639
|
+
},
|
|
1640
|
+
get renderer() {
|
|
1641
|
+
return renderer;
|
|
1642
|
+
},
|
|
1643
|
+
items: () => [],
|
|
1644
|
+
area: () => ({ placements: [], view: sceneView() ?? { eye: [0, 1.7, 6], target: [cx, 1.2, cz] } }),
|
|
1645
|
+
controls: () => controls(),
|
|
1646
|
+
layers: () => benchLayers(),
|
|
1647
|
+
metrics: () => metrics(),
|
|
1648
|
+
scenarios: () => scenarios(),
|
|
1649
|
+
attach(lent) {
|
|
1650
|
+
host = lent;
|
|
1651
|
+
three = lent.THREE;
|
|
1652
|
+
last = null;
|
|
1653
|
+
lent.audio?.register?.(CROWD_SOUND_BANK);
|
|
1654
|
+
const off = lent.bench?.onChange(onChange);
|
|
1655
|
+
const offFrame = lent.onFrame(frame);
|
|
1656
|
+
const offGraphics = lent.graphics?.onChange(() => {
|
|
1657
|
+
if (!renderer) return;
|
|
1658
|
+
lent.scene.remove(renderer.group);
|
|
1659
|
+
renderer.dispose();
|
|
1660
|
+
renderer = null;
|
|
1661
|
+
isBaked = false;
|
|
1662
|
+
});
|
|
1663
|
+
const offFixed = lent.onFrame((step) => world.physics ? stepCrowd(step) : void 0, { phase: "fixedSimulate" });
|
|
1664
|
+
const offBlast = lent.on((event) => {
|
|
1665
|
+
if (event.type !== "explosion") return;
|
|
1666
|
+
world.crowd.blast({ point: event.point, energy: event.energy, radius: event.radius, lethalRadius: event.radius * LETHAL_SHARE, source: event.source, ...event.cause === "part" ? { chain: event.source } : {} });
|
|
1667
|
+
});
|
|
1668
|
+
if (host) removeProps = sceneOf()?.props?.(host, { cx, cz }, options.props ?? {}) ?? null;
|
|
1669
|
+
if (options.eager) queueMicrotask(() => prepareDrawing());
|
|
1670
|
+
return () => {
|
|
1671
|
+
off?.();
|
|
1672
|
+
offFrame();
|
|
1673
|
+
offGraphics?.();
|
|
1674
|
+
offFixed();
|
|
1675
|
+
offBlast();
|
|
1676
|
+
removeProps?.();
|
|
1677
|
+
removeProps = null;
|
|
1678
|
+
if (renderer) lent.scene.remove(renderer.group);
|
|
1679
|
+
if (herdView) lent.scene.remove(herdView.group);
|
|
1680
|
+
renderer?.dispose();
|
|
1681
|
+
renderer = null;
|
|
1682
|
+
herdView = null;
|
|
1683
|
+
host = null;
|
|
1684
|
+
};
|
|
1685
|
+
}
|
|
1686
|
+
};
|
|
1687
|
+
return module;
|
|
1688
|
+
}
|
|
1689
|
+
|
|
1690
|
+
export {
|
|
1691
|
+
VARIETY_DEFAULTS,
|
|
1692
|
+
lookOfAgent,
|
|
1693
|
+
CROWD_RENDER_DEFAULTS,
|
|
1694
|
+
createCrowdInstances,
|
|
1695
|
+
createCrowdRenderer,
|
|
1696
|
+
createHerdView,
|
|
1697
|
+
CROWD_SOUND_BANK,
|
|
1698
|
+
HERD_SOUND_IDS,
|
|
1699
|
+
crowdBenchTextOf,
|
|
1700
|
+
DENSE_WALL,
|
|
1701
|
+
CHAIN_LINE,
|
|
1702
|
+
BUILTIN_SCENES,
|
|
1703
|
+
CROWD_AREA_ID,
|
|
1704
|
+
CROWD_EFFECTS,
|
|
1705
|
+
createCrowdTestModule
|
|
1706
|
+
};
|
|
1707
|
+
//# sourceMappingURL=chunk-PXT6DYGJ.js.map
|