@codincod/ornament 0.0.0-stage → 0.1.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/src/nemos.ts ADDED
@@ -0,0 +1,606 @@
1
+ /**
2
+ * Clownfish, which live on one reef and go nowhere else.
3
+ *
4
+ * The simulation for the animals `reef.ts` houses, kept free of the DOM the way
5
+ * the rest of the ornament is. Everything else that swims in this water is
6
+ * passing through: the shoal crosses the picture and comes back, the visitors
7
+ * arrive and leave. A nemo is the one animal here with an address.
8
+ *
9
+ * ## Why it never leaves
10
+ *
11
+ * Because the real one does not. A clownfish is stung by everything on a reef
12
+ * except the anemone it grew up in, so the anemone is the whole of its world
13
+ * and it spends its life within a few body lengths of it. That is the rarest
14
+ * thing a background can show you: an animal that is somewhere rather than
15
+ * going somewhere. A shoal drifting past says the water is wide, and one fish
16
+ * fussing over the same patch of rock says the water is lived in.
17
+ *
18
+ * It is also the only reason the reef reads as a place at all. A mound with
19
+ * nothing tending it is scenery, and the same mound with something that keeps
20
+ * coming back to one corner of it is a home with a shape you learn.
21
+ *
22
+ * So there is no wandering off. Every station one picks is a place it asked the
23
+ * reef about first, and anything that would take it off the rock is refused
24
+ * rather than corrected later, which is what makes the claim in the name of
25
+ * this file true on every frame and not merely most of them.
26
+ *
27
+ * ## The turn
28
+ *
29
+ * `shoal.ts` forbids a fish to turn round, and the reason is sound: mirroring a
30
+ * drawing mid-swim turns the animal inside out, and easing the mirror instead
31
+ * foreshortens it through edge-on, which reads as paper turning in the wind. A
32
+ * fish crossing the picture never has to turn, so it never does.
33
+ *
34
+ * A fish tied to one anemone has to turn constantly, so the rule cannot hold
35
+ * here and the reason it was made has to be answered instead. What made the
36
+ * foreshortening read as paper was that it happened at speed: an animal going
37
+ * somewhere does not narrow. So a nemo turns only where a real one does, at the
38
+ * end of a run, having stopped. It arrives, hovers, and comes about while it is
39
+ * barely moving, and the drawing narrows through nothing at the moment nothing
40
+ * about it is travelling. See `Nemo.face`.
41
+ *
42
+ * ## Fright is looking, not being told
43
+ *
44
+ * This used to be a call. Something outside worked out that a shark had come
45
+ * near enough to matter and told the fish how frightened to be, which is a
46
+ * director's note, and there is no director in the sea. Nothing tells a
47
+ * clownfish anything. It watches the water, and what it does about what it sees
48
+ * is its own.
49
+ *
50
+ * So a nemo is handed a way of asking what else is out there, `about`, and it
51
+ * looks for itself every step. What it minds is anything bigger than it is, and
52
+ * how far off it minds one from is set by how big that thing is, because a
53
+ * silhouette twice the size is one you notice twice as far away. Something its
54
+ * own size or smaller it ignores, whatever it is doing: a clownfish is not
55
+ * afraid of a shrimp, and it will let a fish its own size swim through the
56
+ * anemone without moving. It weighs all of it by how nearly abreast the thing
57
+ * is swimming, so a shark cruising past the glass leaves a reef in the murk
58
+ * alone however neatly the two line up on the screen.
59
+ *
60
+ * There are three answers and which one a fish gives is its own business.
61
+ * Something merely large, or something hunting a long way off, and it quickens:
62
+ * it stays out, keeps doing what it was doing, and does it faster, which is
63
+ * what a reef looks like when a shape goes over. Something hunting and close,
64
+ * and it gives up on the outing and goes home. A fish that bolted for the
65
+ * anemone every time anything crossed the picture would be a fish nobody ever
66
+ * saw out in the water.
67
+ *
68
+ * ## The third answer is going at it
69
+ *
70
+ * A household is ranked by size and the largest is the female, and she does not
71
+ * hide. She swims out at whatever it is. It is the most reliably observable
72
+ * thing a clownfish does, it is why anybody who has been on a reef with a
73
+ * camera has been bitten by a fish the length of a finger, and a reef where the
74
+ * only answer to a shark is a fish disappearing is a reef with the best of it
75
+ * left out.
76
+ *
77
+ * So `nerve` runs down the household. The female barely minds a shadow and
78
+ * comes out to meet a hunter; the smallest is in the tentacles before anybody
79
+ * else has noticed. What that draws is the picture a reef actually gives you:
80
+ * the little ones gone, and one fish out in front of the anemone facing
81
+ * something a hundred times its size.
82
+ *
83
+ * Nothing has to be wired up for this to work. A water with nothing in it hands
84
+ * back nothing and the fish go about their day.
85
+ *
86
+ * Everything is seeded, so the same seed keeps the same fish on the same rock.
87
+ */
88
+
89
+ import { abreast, type Presence } from "./biome.ts"
90
+ import { makeRandom } from "./perlin.ts"
91
+ import type { Head, Reef } from "./reef.ts"
92
+ import type { Point } from "./seabed.ts"
93
+ import { drawnTilt } from "./shoal.ts"
94
+
95
+ export interface Nemo {
96
+ /** Where it is going, which is always somewhere it asked the reef about. */
97
+ aim: Point
98
+ /** Tail beat, in radians. */
99
+ beat: number
100
+ /** How far into going at something it is, 1 fresh and 0 over it. */
101
+ charge: number
102
+ /** How far into the tentacles it is: 0 out in the water, 1 gone from sight. */
103
+ cover: number
104
+ /**
105
+ * The drawn width, from -1 to 1, whose sign is which way it points.
106
+ *
107
+ * A renderer scales the animal by this and takes the sign as the mirror, so a
108
+ * fish coming about is squeezed to nothing and opens out the other way. It is
109
+ * the mirroring `shoal.ts` refuses to do, allowed here because it only ever
110
+ * happens to a fish that has stopped; see the note at the top of this file.
111
+ */
112
+ face: number
113
+ /** How frightened it is: 1 fresh, 0 over it. */
114
+ fright: number
115
+ /** Seconds left on the station it is holding. */
116
+ hold: number
117
+ /** The anemone it lives in. Its address, its distance and its way out. */
118
+ host: Head
119
+ /** Its own phase, so no two of them waggle together. */
120
+ readonly lane: number
121
+ /** Body length in px, its anemone's distance already taken out of it. */
122
+ length: number
123
+ /**
124
+ * How easily this one is put off, against what it is looking at.
125
+ *
126
+ * Low is steady. It runs down the household from the female, so the same
127
+ * shadow crossing the same anemone empties it of everything but one fish.
128
+ */
129
+ readonly nerve: number
130
+ /** Where it comes in the household. 0 is the female, and she is the largest. */
131
+ readonly rank: number
132
+ /** How far to turn the drawing, in radians, after mirroring it by `face`. */
133
+ tilt: number
134
+ /** px a second. */
135
+ vx: number
136
+ vy: number
137
+ x: number
138
+ y: number
139
+ }
140
+
141
+ export interface Nemos {
142
+ readonly nemos: readonly Nemo[]
143
+ /**
144
+ * Take the reef again, after it has been rebuilt under them.
145
+ *
146
+ * A resize raises a fresh reef, so every anemone a fish knew about is gone,
147
+ * and how many anemones there are is not what it was either. The households
148
+ * are settled again from what the new reef offers.
149
+ */
150
+ resettle(): void
151
+ step(seconds: number): void
152
+ }
153
+
154
+ export interface NemosOptions {
155
+ /**
156
+ * What else is in this water, asked afresh every step.
157
+ *
158
+ * Left out, the water is empty as far as the fish can tell, which is what a
159
+ * reef on its own page is. Whatever supplies it is not commanding anything:
160
+ * it says what is there, and each fish decides for itself whether that is
161
+ * worth minding.
162
+ */
163
+ about?: () => readonly Presence[]
164
+ /**
165
+ * The most fish to settle, for a caller on a budget.
166
+ *
167
+ * Left out, the reef decides: every anemone on it gets its household, and a
168
+ * reef with no anemone on it holds no clownfish at all.
169
+ */
170
+ count?: number
171
+ reef: Reef
172
+ seed: number
173
+ }
174
+
175
+ /**
176
+ * How many fish one anemone keeps, fewest to most.
177
+ *
178
+ * How many there are is a fact about the reef rather than a number a caller
179
+ * thought of. A clownfish anemone holds a household: a large female, her
180
+ * mate, and whatever young are waiting their turn, and they do not leave and
181
+ * they do not join another. So the reef gets the fish its anemones can house,
182
+ * a reef with one anemone has one huddle on one side of it, and nobody has to
183
+ * keep a fish count and an anemone count in step by hand.
184
+ */
185
+ const GROUP_LEAST = 2
186
+
187
+ const GROUP_SPAN = 3
188
+
189
+ /**
190
+ * How much smaller each fish in a household is than the one above it.
191
+ *
192
+ * A clownfish household is a queue, and the queue is visible: they are ranked
193
+ * by size, everybody knows their place in it, and when the female goes the next
194
+ * one up grows into the gap. Drawing them all one size is drawing a shoal that
195
+ * happens to be standing in an anemone.
196
+ */
197
+ const RANK_STEP = 0.16
198
+
199
+ /** The smallest a fish gets however far down the queue it is. */
200
+ const RANK_LEAST = 0.45
201
+
202
+ /**
203
+ * How steady the female is, and how much jumpier each one below her.
204
+ *
205
+ * The spread is the point rather than either end of it. What makes a reef read
206
+ * as inhabited is that one thing crossing it gets several different answers,
207
+ * and the cheapest way to several answers is one figure that is not the same
208
+ * for any two animals.
209
+ */
210
+ const NERVE_BOLD = 0.34
211
+
212
+ const NERVE_RUNG = 0.3
213
+
214
+ const NERVE_JUMPY = 1.2
215
+
216
+ /**
217
+ * How long a fish is against the anemone it lives in, and how much that varies.
218
+ *
219
+ * Against the anemone, because that is what it is always next to and the only
220
+ * thing anybody can judge its size by. It also settles a question a fixed size
221
+ * in px cannot: the reef is drawn against the window it is in, so a clownfish
222
+ * measured in px is a whale on a phone. A hand's length of fish against a
223
+ * dinner plate of host is what the two of them come to in the water, and this
224
+ * keeps it that way at any size.
225
+ */
226
+ const BODY = 0.3
227
+
228
+ const BODY_SPAN = 0.3
229
+
230
+ /**
231
+ * How fast it swims, in body lengths a second, and how much of that it keeps
232
+ * once it has arrived somewhere.
233
+ *
234
+ * The hover is the important half. A clownfish at station is not still, it is
235
+ * holding against the water with its whole body going, and a fish that stopped
236
+ * dead between runs would read as a sticker somebody moved.
237
+ */
238
+ const PACE = 2.6
239
+
240
+ const HOVER = 0.35
241
+
242
+ /** How much faster a frightened one goes. */
243
+ const BOLT = 2.2
244
+
245
+ /** How long it holds a station before choosing another, in seconds. */
246
+ const HOLD_LEAST = 0.6
247
+
248
+ const HOLD_SPAN = 1.9
249
+
250
+ /**
251
+ * How far from its anemone it will go, in body lengths.
252
+ *
253
+ * Short, and shorter than the reef is wide, which is the point of it. The fish
254
+ * is bound to the reef because it is bound to one anemone on the reef, so a
255
+ * reef with anemones at both ends has fish at both ends and a bare stretch in
256
+ * between, rather than a herd milling evenly over the whole rock.
257
+ */
258
+ const RANGE = 9
259
+
260
+ /** How close counts as arrived, in body lengths. */
261
+ const ARRIVE = 0.6
262
+
263
+ /** How far above the rock it keeps, in body lengths. */
264
+ const CLEAR = 0.9
265
+
266
+ /** How quickly it eases onto the line it wants, as a share a second. */
267
+ const EASE = 4.5
268
+
269
+ /**
270
+ * How far it waggles off its line, in body lengths a second.
271
+ *
272
+ * A clownfish does not hold a line. It rows itself along on its pectorals with
273
+ * its whole body rocking, so the path is a wobble with a direction to it, and
274
+ * the wobble is most of what tells this animal from a fish that is going
275
+ * somewhere. It is taken across the line of travel rather than up the picture,
276
+ * because a fish rising to the top of the anemone rocks side to side as much as
277
+ * one crossing it does.
278
+ */
279
+ const WAGGLE = 1.4
280
+
281
+ /** How far it goes per beat of its tail, in body lengths. */
282
+ const STRIDE = 0.42
283
+
284
+ /**
285
+ * How fast it comes about, in units of `Nemo.face` a second.
286
+ *
287
+ * Quick. The turn is the one moment the drawing is not an animal, so what it
288
+ * has to be is over: slow enough that nothing jumps between two frames, fast
289
+ * enough that nobody watching gets a good look at the edge-on frame.
290
+ */
291
+ const FLIP = 7
292
+
293
+ /** How slowly it has to be going before it will come about, in body lengths. */
294
+ const FLIP_BELOW = 0.9
295
+
296
+ /** How long a fright lasts, in seconds. */
297
+ const FRIGHT_FADE = 4.5
298
+
299
+ /**
300
+ * How far off a thing is minded from, against how big across it is.
301
+ *
302
+ * Against its size, because that is what being noticed actually depends on. A
303
+ * fixed reach makes a passing turtle and a passing whale equally alarming at
304
+ * the same distance and neither of them alarming at all a moment before, and
305
+ * what that reads as is a trigger line drawn on the water.
306
+ */
307
+ const NOTICE = 7
308
+
309
+ /** How much bigger than the fish a thing has to be before it minds it. */
310
+ const MINDS = 1.6
311
+
312
+ /**
313
+ * How alarming something with no menace about it is, at its worst.
314
+ *
315
+ * Not nothing. A turtle going over is a shadow going over, and every small fish
316
+ * on a reef looks up at a shadow; what it does not do is leave. So the plain
317
+ * bulk of a thing buys a quickening and never more than one, and the rest of
318
+ * the reading is what it looks like it eats.
319
+ */
320
+ const BULK = 0.4
321
+
322
+ /**
323
+ * How frightened it has to be to give up on where it was going.
324
+ *
325
+ * Above what the bulk of a harmless thing can buy on its own, deliberately. A
326
+ * shadow quickens a fish and a hunter sends it home, and the line between them
327
+ * is here.
328
+ */
329
+ const FRIGHT_HOME = 0.55
330
+
331
+ /** How fast it works its way into the tentacles and back out, a share a second. */
332
+ const COVER_IN = 2.4
333
+
334
+ const COVER_OUT = 0.7
335
+
336
+ /** How close to home it has to be to get inside, in body lengths. */
337
+ const COVER_NEAR = 1.2
338
+
339
+ /**
340
+ * How alarming a thing has to be before the female goes out to meet it.
341
+ *
342
+ * Read off what she is looking at rather than off her own fright, because she
343
+ * is the one fish here who barely has any, and a threshold on the fright of an
344
+ * animal picked for not being frightened would never be crossed.
345
+ */
346
+ const CHARGE_AT = 0.3
347
+
348
+ /** How long she keeps at it after the thing has gone, in seconds. */
349
+ const CHARGE_FADE = 2.4
350
+
351
+ /** How many places it will ask the reef about before settling for home. */
352
+ const TRIES = 12
353
+
354
+ export function createNemos(options: NemosOptions): Nemos {
355
+ const reef = options.reef
356
+ const random = makeRandom(options.seed ^ 0x6f2d)
357
+
358
+ const nemos: Nemo[] = []
359
+
360
+ /** The anemones on this reef, which is the whole of where a fish may live. */
361
+ const anemones = (): Head[] => reef.heads.filter((one) => one.kind === "anemone")
362
+
363
+ /**
364
+ * A station somewhere over the reef, or home if the reef offered nothing.
365
+ *
366
+ * The fish proposes and the reef disposes. Nothing here works out where the
367
+ * rock is or how far the reef reaches; it asks, and asks again if the answer
368
+ * was no, which is why the same routine serves a reef of any shape.
369
+ */
370
+ function station(one: Nemo): Point {
371
+ const reach = RANGE * one.length
372
+
373
+ for (let tries = 0; tries < TRIES; tries++) {
374
+ const angle = random() * Math.PI * 2
375
+ const out = Math.sqrt(random()) * reach
376
+ const x = one.host.x + Math.cos(angle) * out
377
+ const y = one.host.y + Math.sin(angle) * out
378
+
379
+ if (!reef.holds(x, y, one.host.depth)) continue
380
+ if (y > reef.surfaceAt(x, one.host.depth) - CLEAR * one.length) continue
381
+
382
+ return { x, y }
383
+ }
384
+
385
+ return { x: one.host.x, y: one.host.y - CLEAR * one.length }
386
+ }
387
+
388
+ /** One fish, at its place in the queue, facing whichever way it turned out. */
389
+ function settle(host: Head, rank: number): Nemo {
390
+ const queue = Math.max(RANK_LEAST, 1 - rank * RANK_STEP)
391
+ const length = host.span * BODY * queue * (1 - BODY_SPAN / 2 + random() * BODY_SPAN)
392
+ const x = host.x + (random() - 0.5) * host.span
393
+ // Above the rock at its own x rather than above the anemone's. A household
394
+ // is spread across the width of its host, and the rock under one end of
395
+ // that spread is not the rock under the middle of it.
396
+ const y = Math.min(host.y, reef.surfaceAt(x, host.depth)) - CLEAR * length
397
+
398
+ return {
399
+ aim: { x, y },
400
+ beat: random() * Math.PI * 2,
401
+ charge: 0,
402
+ cover: 0,
403
+ face: random() < 0.5 ? -1 : 1,
404
+ fright: 0,
405
+ hold: random() * HOLD_SPAN,
406
+ host,
407
+ lane: random() * Math.PI * 2,
408
+ length,
409
+ nerve: Math.min(NERVE_JUMPY, NERVE_BOLD + rank * NERVE_RUNG),
410
+ rank,
411
+ tilt: 0,
412
+ vx: 0,
413
+ vy: 0,
414
+ x,
415
+ y,
416
+ }
417
+ }
418
+
419
+ function populate(): void {
420
+ const most = Math.max(0, options.count ?? Number.POSITIVE_INFINITY)
421
+ nemos.length = 0
422
+
423
+ for (const host of anemones()) {
424
+ const group = GROUP_LEAST + Math.floor(random() * (GROUP_SPAN + 1))
425
+ for (let made = 0; made < group && nemos.length < most; made++) {
426
+ nemos.push(settle(host, made))
427
+ }
428
+ }
429
+ }
430
+
431
+ /** Onto the line it wants, at the speed it wants, with its tail going. */
432
+ function swim(one: Nemo, dt: number): void {
433
+ const toX = one.aim.x - one.x
434
+ const toY = one.aim.y - one.y
435
+ const away = Math.hypot(toX, toY)
436
+ const near = away < ARRIVE * one.length
437
+
438
+ const stirred = Math.max(one.fright, one.charge)
439
+ const pace = (near ? HOVER : 1) * PACE * (1 + stirred * BOLT) * one.length
440
+ const wantX = away > 0 ? (toX / away) * pace : 0
441
+ const wantY = away > 0 ? (toY / away) * pace : 0
442
+
443
+ const rate = Math.min(1, EASE * dt)
444
+ one.vx += (wantX - one.vx) * rate
445
+ one.vy += (wantY - one.vy) * rate
446
+
447
+ const speed = Math.hypot(one.vx, one.vy)
448
+ one.beat += (speed / Math.max(1, STRIDE * one.length)) * dt * Math.PI * 2
449
+
450
+ const roll = Math.sin(one.beat + one.lane) * WAGGLE * one.length
451
+ const sideX = speed > 0 ? -(one.vy / speed) * roll : 0
452
+ const sideY = speed > 0 ? (one.vx / speed) * roll : 0
453
+
454
+ one.x += (one.vx + sideX) * dt
455
+ one.y += (one.vy + sideY) * dt
456
+ }
457
+
458
+ /**
459
+ * Back onto the reef, if that step took it off.
460
+ *
461
+ * A refusal rather than a correction, and it is given the place the fish came
462
+ * from rather than working one out from the speed, because the waggle moves
463
+ * it as well and a step undone by halves lands somewhere it never was. Where
464
+ * it wanted to go was somewhere the reef agreed to, so this only fires when
465
+ * the reef has moved under it or the waggle threw it wide, and in both cases
466
+ * the honest answer is that the fish never went there.
467
+ */
468
+ function keep(one: Nemo, fromX: number, fromY: number): void {
469
+ const top = reef.surfaceAt(one.x, one.host.depth) - CLEAR * one.length
470
+
471
+ if (one.y > top && reef.holds(one.x, top, one.host.depth)) {
472
+ one.y = top
473
+ one.vy = Math.min(one.vy, 0)
474
+ }
475
+
476
+ if (reef.holds(one.x, one.y, one.host.depth)) return
477
+
478
+ one.x = fromX
479
+ one.y = fromY
480
+ one.vx = 0
481
+ one.vy = 0
482
+ one.hold = 0
483
+ one.aim = { x: one.host.x, y: one.host.y - CLEAR * one.length }
484
+ }
485
+
486
+ /** Coming about, which it will only do once it has stopped. */
487
+ function turn(one: Nemo, dt: number): void {
488
+ const speed = Math.hypot(one.vx, one.vy)
489
+ const slow = speed < FLIP_BELOW * one.length
490
+ const want = slow && Math.abs(one.vx) > 0 ? Math.sign(one.vx) : Math.sign(one.face) || 1
491
+ const step = FLIP * dt
492
+
493
+ one.face += Math.min(Math.max(want - one.face, -step), step)
494
+ one.tilt = drawnTilt(Math.atan2(one.vy, Math.abs(one.vx)), Math.sign(one.face) || 1)
495
+ }
496
+
497
+ /**
498
+ * How alarmed this fish is by what it can see of the water, right now.
499
+ *
500
+ * Its own reading, taken fresh every step rather than remembered, so a thing
501
+ * that came near and went away leaves nothing behind but the fright already
502
+ * fading. The nearest thing wins; a fish being approached by two of something
503
+ * is not twice as frightened, it is frightened.
504
+ */
505
+ function minded(one: Nemo, water: readonly Presence[]): { alarm: number; at: Presence | null } {
506
+ let alarm = 0
507
+ let at: Presence | null = null
508
+
509
+ for (const thing of water) {
510
+ if (thing.size < one.length * MINDS) continue
511
+
512
+ const reach = thing.size * NOTICE
513
+ const away = Math.hypot(one.x - thing.x, one.y - thing.y)
514
+ if (away >= reach) continue
515
+
516
+ const near = (1 - away / reach) * abreast(one.host.depth, thing.depth)
517
+ const worry = near * (BULK + (thing.menace ?? 0) * (1 - BULK))
518
+ if (worry <= alarm) continue
519
+
520
+ alarm = worry
521
+ at = thing
522
+ }
523
+
524
+ return { alarm, at }
525
+ }
526
+
527
+ /**
528
+ * Where the female goes to meet something, which is towards it.
529
+ *
530
+ * As far towards it as the reef will let her and no further, so the fish that
531
+ * never leaves the rock does not leave the rock to see something off. She
532
+ * works out along that line until the reef stops agreeing, which is the same
533
+ * asking `station` does and the same refusal.
534
+ */
535
+ function meet(one: Nemo, at: Presence): Point {
536
+ const home = { x: one.host.x, y: one.host.y - CLEAR * one.length }
537
+ const toX = at.x - one.host.x
538
+ const toY = at.y - one.host.y
539
+ const away = Math.hypot(toX, toY)
540
+ if (away < 1) return home
541
+
542
+ const out = Math.min(away, RANGE * one.length)
543
+ for (let back = TRIES; back >= 1; back--) {
544
+ const reach = out * (back / TRIES)
545
+ const x = one.host.x + (toX / away) * reach
546
+ const y = one.host.y + (toY / away) * reach
547
+
548
+ if (!reef.holds(x, y, one.host.depth)) continue
549
+ if (y > reef.surfaceAt(x, one.host.depth) - CLEAR * one.length) continue
550
+
551
+ return { x, y }
552
+ }
553
+
554
+ return home
555
+ }
556
+
557
+ /** Into the tentacles while it is frightened, and out again once it is not. */
558
+ function hide(one: Nemo, dt: number): void {
559
+ const home = Math.hypot(one.x - one.host.x, one.y - one.host.y) < COVER_NEAR * one.length
560
+ const want = one.fright > FRIGHT_HOME && home ? 1 : 0
561
+ const rate = (want > one.cover ? COVER_IN : COVER_OUT) * dt
562
+
563
+ one.cover += Math.min(Math.max(want - one.cover, -rate), rate)
564
+ }
565
+
566
+ populate()
567
+
568
+ return {
569
+ nemos,
570
+
571
+ resettle() {
572
+ populate()
573
+ },
574
+
575
+ step(seconds) {
576
+ const dt = Math.min(Math.max(seconds, 0), 0.1)
577
+ const water = options.about?.() ?? []
578
+
579
+ for (const one of nemos) {
580
+ const seen = minded(one, water)
581
+ const goes = one.rank === 0 && seen.at != null && seen.alarm > CHARGE_AT
582
+
583
+ one.fright = Math.max(0, Math.max(one.fright - dt / FRIGHT_FADE, seen.alarm * one.nerve))
584
+ one.charge = Math.max(0, Math.max(one.charge - dt / CHARGE_FADE, goes ? seen.alarm : 0))
585
+ one.hold -= dt
586
+
587
+ if (goes && seen.at != null) {
588
+ one.aim = meet(one, seen.at)
589
+ } else if (one.fright > FRIGHT_HOME) {
590
+ one.aim = { x: one.host.x, y: one.host.y - CLEAR * one.length }
591
+ } else if (one.hold <= 0) {
592
+ one.hold = HOLD_LEAST + random() * HOLD_SPAN
593
+ one.aim = station(one)
594
+ }
595
+
596
+ const fromX = one.x
597
+ const fromY = one.y
598
+
599
+ swim(one, dt)
600
+ keep(one, fromX, fromY)
601
+ turn(one, dt)
602
+ hide(one, dt)
603
+ }
604
+ },
605
+ }
606
+ }