@kvmet/beefwife 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/beefwife.js ADDED
@@ -0,0 +1,3182 @@
1
+ /*! Beefwife v0.1.0. Generated from beefwife/src; do not edit.
2
+ (c) Kristen Metcalfe, MPL-2.0. */
3
+ var Beefwife = (function(pixi_js) {
4
+
5
+ // \0rolldown/runtime.js
6
+ var __create = Object.create;
7
+ var __defProp = Object.defineProperty;
8
+ var __name = (target, value) => __defProp(target, "name", {
9
+ value,
10
+ configurable: true
11
+ });
12
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
13
+ var __getOwnPropNames = Object.getOwnPropertyNames;
14
+ var __getProtoOf = Object.getPrototypeOf;
15
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
16
+ var __exportAll = (all, flags) => {
17
+ let target = Object.setPrototypeOf({}, null);
18
+ for (var name in all) {
19
+ __defProp(target, name, {
20
+ get: all[name],
21
+ enumerable: true
22
+ });
23
+ }
24
+ if (!(flags & 1)) {
25
+ __defProp(target, Symbol.toStringTag, { value: "Module" });
26
+ }
27
+ return flags & 2 ? target : Object.preventExtensions(target);
28
+ };
29
+ var __copyProps = (to, from, except, desc) => {
30
+ if (from && typeof from === "object" || typeof from === "function") {
31
+ for (var keys = __getOwnPropNames(from), i = 0, n = keys.length, key; i < n; i++) {
32
+ key = keys[i];
33
+ if (!__hasOwnProp.call(to, key) && key !== except) {
34
+ __defProp(to, key, {
35
+ get: ((k) => from[k]).bind(null, key),
36
+ enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable
37
+ });
38
+ }
39
+ }
40
+ }
41
+ return to;
42
+ };
43
+ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(isNodeMode || !mod || !mod.__esModule || !__hasOwnProp.call(mod, "default") ? __defProp(target, "default", {
44
+ value: mod,
45
+ enumerable: true
46
+ }) : target, mod));
47
+
48
+ pixi_js = __toESM(pixi_js, 1);
49
+
50
+ // src/pixi.mjs
51
+ /**
52
+ * The renderer seam. Pixi is a peer, not a dependency: the module build imports
53
+ * it and the classic-script build reads the page's global, which may be absent.
54
+ * Every Pixi reference in the library arrives through here, so `available` is
55
+ * the one place that decides whether a beefwife draws or only simulates.
56
+ */
57
+ var PIXI = pixi_js;
58
+ var available = typeof PIXI?.Container === "function";
59
+
60
+ // src/limits.mjs
61
+ var MAX_WORLD_COORDINATE = 1e9;
62
+ var MIN_PIXEL_RESOLUTION = 1e-6;
63
+ var MAX_PIXEL_RESOLUTION = 1e6;
64
+ var MAX_PERSPECTIVE = 1e6;
65
+ var PHYSICS_STEP = 1 / 60;
66
+
67
+ // src/schema.mjs
68
+ /**
69
+ * The v1 schema tree: every field a beefwife descriptor may hold, its kind,
70
+ * and its bounds. Length-dimensioned fields carry `length`, the power of the
71
+ * resize factor they scale by. Reading and resizing a descriptor against this
72
+ * tree is beefwife-descriptor.js.
73
+ */
74
+ var VERSION = 1;
75
+ var SECTIONS = [
76
+ "head",
77
+ "trunk",
78
+ "tail"
79
+ ];
80
+ var ID_PATTERN = /^[A-Za-z][A-Za-z0-9_-]{0,63}$/;
81
+ var NAME_PATTERN = /^[a-z0-9][a-z0-9-]{0,63}$/;
82
+ var LIMITS = Object.freeze({
83
+ name: 64,
84
+ definitions: 256,
85
+ chunks: 256,
86
+ placements: 512,
87
+ instances: 512,
88
+ path: 65536,
89
+ pathTotal: 1048576
90
+ });
91
+ var number = (min, max, integer = false) => ({
92
+ kind: "number",
93
+ min,
94
+ max,
95
+ integer
96
+ });
97
+ var px = (min, max) => ({
98
+ ...number(min, max),
99
+ length: 1
100
+ });
101
+ var perPx = (min, max) => ({
102
+ ...number(min, max),
103
+ length: -1
104
+ });
105
+ var string = (minLength, maxLength, pattern = null) => ({
106
+ kind: "string",
107
+ minLength,
108
+ maxLength,
109
+ pattern
110
+ });
111
+ var choice = (...values) => ({
112
+ kind: "choice",
113
+ values
114
+ });
115
+ var literal = (value) => ({
116
+ kind: "literal",
117
+ value
118
+ });
119
+ var nullable = (item) => ({
120
+ kind: "nullable",
121
+ item
122
+ });
123
+ var object = (fields) => ({
124
+ kind: "object",
125
+ fields
126
+ });
127
+ var array = (item, maxLength) => ({
128
+ kind: "array",
129
+ item,
130
+ maxLength
131
+ });
132
+ var record = (item, minLength = 0) => ({
133
+ kind: "record",
134
+ item,
135
+ minLength,
136
+ maxLength: LIMITS.definitions
137
+ });
138
+ var id = string(1, 64, ID_PATTERN);
139
+ var ratio = number(0, 1);
140
+ var pxScale = px(.001, 1e3);
141
+ var ratioScale = number(.001, 100);
142
+ var distance = px(1e-6, 1e4);
143
+ var offset = px(-1e4, 1e4);
144
+ var nonBlank = (node) => ({
145
+ ...node,
146
+ blank: false
147
+ });
148
+ var colour = nullable(nonBlank(string(1, 256)));
149
+ var material = object({
150
+ velocityRetention: ratio,
151
+ jointCorrection: ratio,
152
+ linkCorrection: number(.001, 1),
153
+ grip: object({
154
+ forward: ratio,
155
+ backward: ratio,
156
+ lateral: ratio
157
+ })
158
+ });
159
+ var shape = object({ path: nonBlank(string(1, LIMITS.path)) });
160
+ var paint = object({
161
+ fill: colour,
162
+ stroke: nullable(object({
163
+ colour: nonBlank(string(1, 256)),
164
+ width: px(0, 1e3)
165
+ }))
166
+ });
167
+ var span = object({
168
+ start: px(0, 1e3),
169
+ end: px(0, 1e3)
170
+ });
171
+ var sectionOf = (minChunks) => object({
172
+ chunks: number(minChunks, LIMITS.chunks, true),
173
+ spacing: px(1e-6, 1e3),
174
+ material: id,
175
+ motionScale: object({
176
+ bend: number(0, 4),
177
+ thrust: number(0, 4),
178
+ gather: number(0, 4),
179
+ contact: number(0, 4)
180
+ }),
181
+ profile: object({
182
+ ribbonWidth: span,
183
+ plateScale: object({
184
+ start: number(0, 100),
185
+ end: number(0, 100)
186
+ })
187
+ })
188
+ });
189
+ var anchor = object({
190
+ section: nullable(choice(...SECTIONS)),
191
+ from: choice("head", "tail"),
192
+ offset: number(0, LIMITS.chunks - 1, true)
193
+ });
194
+ var repeat = object({
195
+ count: nullable(number(1, LIMITS.chunks, true)),
196
+ step: number(1, LIMITS.chunks, true)
197
+ });
198
+ var plate = object({
199
+ id,
200
+ shape: id,
201
+ paint: id,
202
+ at: anchor,
203
+ repeat,
204
+ scale: pxScale
205
+ });
206
+ var ornament = object({
207
+ id,
208
+ shape: id,
209
+ paint: id,
210
+ at: anchor,
211
+ repeat,
212
+ side: choice("left", "right", "both"),
213
+ layer: choice("under", "over"),
214
+ offset: object({
215
+ forward: offset,
216
+ outward: offset
217
+ }),
218
+ angleDegrees: number(-180, 180),
219
+ scale: pxScale,
220
+ source: ratio,
221
+ react: number(-4, 4),
222
+ recover: number(0, 1e3),
223
+ wobble: ratio
224
+ });
225
+ var schema = object({
226
+ schemaVersion: literal(1),
227
+ name: string(1, LIMITS.name, NAME_PATTERN),
228
+ definitions: object({
229
+ materials: record(material, 1),
230
+ shapes: record(shape, 1),
231
+ paints: record(paint, 1)
232
+ }),
233
+ gait: object({
234
+ cyclesPerSecond: number(0, 15),
235
+ phaseLagRadiansPerPixel: perPx(-1e3, 1e3),
236
+ bend: object({
237
+ amplitude: number(0, 10),
238
+ harmonic: number(1, 8, true)
239
+ }),
240
+ thrust: object({
241
+ acceleration: px(0, 1e6),
242
+ harmonic: number(1, 8, true),
243
+ phaseOffset: number(-Math.PI, Math.PI),
244
+ dutyCycle: number(.01, 1)
245
+ }),
246
+ gather: object({
247
+ amplitude: number(0, .95),
248
+ harmonic: number(1, 8, true),
249
+ phaseOffset: number(-Math.PI, Math.PI)
250
+ }),
251
+ contact: object({
252
+ amplitude: ratio,
253
+ harmonic: number(1, 8, true),
254
+ phaseOffset: number(-Math.PI, Math.PI),
255
+ dutyCycle: number(.01, 1)
256
+ })
257
+ }),
258
+ chain: object({
259
+ physics: object({
260
+ autoLift: object({
261
+ amount: ratio,
262
+ share: ratio,
263
+ rate: number(0, 1e3)
264
+ }),
265
+ steering: object({
266
+ gain: number(0, 100),
267
+ limit: number(0, Math.PI),
268
+ rate: number(0, 1e3)
269
+ })
270
+ }),
271
+ breathing: ratio,
272
+ sections: object({
273
+ head: sectionOf(1),
274
+ trunk: sectionOf(1),
275
+ tail: sectionOf(0)
276
+ }),
277
+ skin: object({
278
+ loadScale: number(-1, 10),
279
+ ribbon: object({ paint: id }),
280
+ plates: array(plate, LIMITS.chunks),
281
+ ornaments: array(ornament, LIMITS.placements)
282
+ })
283
+ }),
284
+ legs: object({
285
+ section: choice(...SECTIONS),
286
+ pairs: number(0, 128, true),
287
+ reach: distance,
288
+ spread: number(0, 4),
289
+ lead: ratio,
290
+ fold: ratio,
291
+ jointBend: number(-1, 1),
292
+ jointLean: number(-1, 1),
293
+ jointLeanCenter: number(-1, 1),
294
+ sidePhase: ratio,
295
+ liftThreshold: ratio,
296
+ swingCycles: number(.001, 60),
297
+ swingArc: number(0, 4),
298
+ jitter: ratio,
299
+ skin: object({
300
+ limbPaint: id,
301
+ limbWidth: px(0, 1e3),
302
+ foot: object({
303
+ shape: id,
304
+ paint: id,
305
+ scale: pxScale,
306
+ plantedScale: ratioScale
307
+ })
308
+ })
309
+ })
310
+ });
311
+
312
+ // src/descriptor.mjs
313
+ /**
314
+ * Canonical JSON contract for a beefwife. Definitions are descriptor-local:
315
+ * sections link physical materials, while visual placements link shapes and
316
+ * paints. `read` validates and returns an owned value in canonical key order.
317
+ * `scale` resizes a creature by transforming every length-dimensioned field.
318
+ * `bounds` reports what the schema enforces for one field.
319
+ */
320
+ var descriptor_exports = /* @__PURE__ */ __exportAll({
321
+ ID_PATTERN: () => ID_PATTERN,
322
+ LIMITS: () => LIMITS,
323
+ NAME_PATTERN: () => NAME_PATTERN,
324
+ VERSION: () => 1,
325
+ bounds: () => bounds,
326
+ parse: () => parse,
327
+ read: () => read,
328
+ scale: () => scale,
329
+ stringify: () => stringify
330
+ });
331
+ var fail = (path, message) => {
332
+ throw new Error(`${path}: ${message}`);
333
+ };
334
+ var plainObject$1 = /* @__PURE__ */ __name((value, path) => {
335
+ if (!value || typeof value !== "object" || Array.isArray(value)) fail(path, "must be an object");
336
+ const prototype = Object.getPrototypeOf(value);
337
+ if (prototype !== Object.prototype && prototype !== null) fail(path, "must be a plain JSON object");
338
+ }, "plainObject");
339
+ var ownKeys = (value, path) => {
340
+ const keys = Reflect.ownKeys(value);
341
+ if (keys.some((key) => typeof key !== "string")) fail(path, "must not contain symbol keys");
342
+ keys.forEach((key) => {
343
+ const descriptor = Object.getOwnPropertyDescriptor(value, key);
344
+ if (!descriptor.enumerable || !("value" in descriptor)) fail(`${path}.${key}`, "must be an enumerable data property");
345
+ });
346
+ return keys;
347
+ };
348
+ var readNode = (node, value, path, ancestors) => {
349
+ if (node.kind === "nullable") return value === null ? null : readNode(node.item, value, path, ancestors);
350
+ if (node.kind === "literal") {
351
+ if (value !== node.value) fail(path, `must equal ${node.value}`);
352
+ return value;
353
+ }
354
+ if (node.kind === "choice") {
355
+ if (!node.values.includes(value)) fail(path, `must be one of ${node.values.join(", ")}`);
356
+ return value;
357
+ }
358
+ if (node.kind === "number") {
359
+ if (typeof value !== "number" || !Number.isFinite(value)) fail(path, "must be a finite number");
360
+ if (node.integer && !Number.isInteger(value)) fail(path, "must be an integer");
361
+ if (value < node.min || value > node.max) fail(path, `must be between ${node.min} and ${node.max}`);
362
+ return Object.is(value, -0) ? 0 : value;
363
+ }
364
+ if (node.kind === "string") {
365
+ if (typeof value !== "string") fail(path, "must be a string");
366
+ if (value.length < node.minLength || value.length > node.maxLength) fail(path, `must contain ${node.minLength} to ${node.maxLength} characters`);
367
+ if (node.pattern && !node.pattern.test(value)) fail(path, "contains characters that are not allowed");
368
+ return value;
369
+ }
370
+ if (node.kind === "array") {
371
+ if (!Array.isArray(value)) fail(path, "must be an array");
372
+ } else plainObject$1(value, path);
373
+ if (ancestors.has(value)) fail(path, "must not contain a cycle");
374
+ ancestors.add(value);
375
+ try {
376
+ if (node.kind === "array") {
377
+ if (value.length > node.maxLength) fail(path, `must contain at most ${node.maxLength} entries`);
378
+ const keys = Reflect.ownKeys(value).filter((key) => key !== "length");
379
+ if (keys.some((key) => typeof key !== "string")) fail(path, "must not contain symbol keys");
380
+ keys.forEach((key) => {
381
+ const descriptor = Object.getOwnPropertyDescriptor(value, key);
382
+ if (!descriptor.enumerable || !("value" in descriptor)) fail(`${path}.${String(key)}`, "must be an enumerable data property");
383
+ });
384
+ for (let i = 0; i < value.length; i++) if (!Object.hasOwn(value, i)) fail(`${path}[${i}]`, "is missing");
385
+ if (keys.some((key) => !/^(0|[1-9][0-9]*)$/.test(key))) fail(path, "must not contain named properties");
386
+ return value.map((item, i) => readNode(node.item, item, `${path}[${i}]`, ancestors));
387
+ }
388
+ const keys = ownKeys(value, path);
389
+ if (node.kind === "record") {
390
+ if (keys.length < node.minLength || keys.length > node.maxLength) fail(path, `must contain ${node.minLength} to ${node.maxLength} definitions`);
391
+ const out = {};
392
+ keys.sort().forEach((key) => {
393
+ if (!ID_PATTERN.test(key)) fail(`${path}.${key}`, "has an invalid id");
394
+ out[key] = readNode(node.item, value[key], `${path}.${key}`, ancestors);
395
+ });
396
+ return out;
397
+ }
398
+ const expected = Object.keys(node.fields);
399
+ keys.forEach((key) => {
400
+ if (!Object.hasOwn(node.fields, key)) fail(`${path}.${key}`, "is unknown");
401
+ });
402
+ const out = {};
403
+ expected.forEach((key) => {
404
+ if (!Object.hasOwn(value, key)) fail(`${path}.${key}`, "is required");
405
+ out[key] = readNode(node.fields[key], value[key], `${path}.${key}`, ancestors);
406
+ });
407
+ return out;
408
+ } finally {
409
+ ancestors.delete(value);
410
+ }
411
+ };
412
+ var reference = (table, id, path) => {
413
+ if (!Object.hasOwn(table, id)) fail(path, `references missing "${id}"`);
414
+ };
415
+ var resolvedChunks = (descriptor, placement, path) => {
416
+ const sections = descriptor.chain.sections;
417
+ const offsets = {
418
+ head: 0,
419
+ trunk: sections.head.chunks,
420
+ tail: sections.head.chunks + sections.trunk.chunks
421
+ };
422
+ const section = placement.at.section;
423
+ const length = section ? sections[section].chunks : SECTIONS.reduce((sum, name) => sum + sections[name].chunks, 0);
424
+ if (!length) fail(`${path}.at`, "cannot address an empty scope");
425
+ const offset = placement.at.offset;
426
+ if (offset >= length) fail(`${path}.at.offset`, "falls outside its scope");
427
+ const direction = placement.at.from === "head" ? 1 : -1;
428
+ const localStart = direction > 0 ? offset : length - 1 - offset;
429
+ const available = Math.floor((length - 1 - offset) / placement.repeat.step) + 1;
430
+ const count = placement.repeat.count ?? available;
431
+ if (count > available) fail(`${path}.repeat.count`, "runs outside its scope");
432
+ const base = section ? offsets[section] : 0;
433
+ return Array.from({ length: count }, (_, i) => base + localStart + direction * i * placement.repeat.step);
434
+ };
435
+ var validate = (descriptor) => {
436
+ const { definitions, chain, gait, legs } = descriptor;
437
+ const sections = chain.sections;
438
+ const total = SECTIONS.reduce((sum, name) => sum + sections[name].chunks, 0);
439
+ if (total < 2 || total > LIMITS.chunks) fail("$.chain.sections", `must contain 2 to ${LIMITS.chunks} chunks`);
440
+ SECTIONS.forEach((name) => reference(definitions.materials, sections[name].material, `$.chain.sections.${name}.material`));
441
+ SECTIONS.forEach((name) => {
442
+ const scale = sections[name].motionScale;
443
+ if (descriptor.gait.gather.amplitude * scale.gather >= 1) fail(`$.chain.sections.${name}.motionScale.gather`, "makes the gathered link length zero or negative");
444
+ if (descriptor.gait.contact.amplitude * scale.contact > 1) fail(`$.chain.sections.${name}.motionScale.contact`, "makes ground contact negative");
445
+ });
446
+ [
447
+ [
448
+ definitions.paints,
449
+ chain.skin.ribbon.paint,
450
+ "$.chain.skin.ribbon.paint"
451
+ ],
452
+ [
453
+ definitions.paints,
454
+ legs.skin.limbPaint,
455
+ "$.legs.skin.limbPaint"
456
+ ],
457
+ [
458
+ definitions.shapes,
459
+ legs.skin.foot.shape,
460
+ "$.legs.skin.foot.shape"
461
+ ],
462
+ [
463
+ definitions.paints,
464
+ legs.skin.foot.paint,
465
+ "$.legs.skin.foot.paint"
466
+ ]
467
+ ].forEach((args) => reference(...args));
468
+ const placementIds = /* @__PURE__ */ new Set();
469
+ const occupied = /* @__PURE__ */ new Map();
470
+ chain.skin.plates.forEach((entry, i) => {
471
+ const path = `$.chain.skin.plates[${i}]`;
472
+ if (placementIds.has(entry.id)) fail(`${path}.id`, "must be unique");
473
+ placementIds.add(entry.id);
474
+ reference(definitions.shapes, entry.shape, `${path}.shape`);
475
+ reference(definitions.paints, entry.paint, `${path}.paint`);
476
+ resolvedChunks(descriptor, entry, path).forEach((chunk) => {
477
+ if (occupied.has(chunk)) fail(`${path}.at`, `overlaps plate "${occupied.get(chunk)}" at chunk ${chunk}`);
478
+ occupied.set(chunk, entry.id);
479
+ });
480
+ });
481
+ let ornamentInstances = 0;
482
+ chain.skin.ornaments.forEach((entry, i) => {
483
+ const path = `$.chain.skin.ornaments[${i}]`;
484
+ if (placementIds.has(entry.id)) fail(`${path}.id`, "must be unique");
485
+ placementIds.add(entry.id);
486
+ reference(definitions.shapes, entry.shape, `${path}.shape`);
487
+ reference(definitions.paints, entry.paint, `${path}.paint`);
488
+ ornamentInstances += resolvedChunks(descriptor, entry, path).length * (entry.side === "both" ? 2 : 1);
489
+ if (ornamentInstances > LIMITS.instances) fail("$.chain.skin.ornaments", `must expand to at most ${LIMITS.instances} instances`);
490
+ });
491
+ [
492
+ "bend",
493
+ "thrust",
494
+ "gather",
495
+ "contact"
496
+ ].forEach((channel) => {
497
+ if (gait.cyclesPerSecond * gait[channel].harmonic > 15) fail(`$.gait.${channel}.harmonic`, `times cyclesPerSecond must be at most ${15}`);
498
+ });
499
+ if (legs.pairs && !sections[legs.section].chunks) fail("$.legs.section", "cannot attach legs to an empty section");
500
+ Object.entries(definitions.paints).forEach(([name, entry]) => {
501
+ if (entry.fill !== null && !entry.fill.trim()) fail(`$.definitions.paints.${name}.fill`, "must not be blank");
502
+ if (entry.stroke !== null && !entry.stroke.colour.trim()) fail(`$.definitions.paints.${name}.stroke.colour`, "must not be blank");
503
+ if (entry.fill === null && (entry.stroke === null || !entry.stroke.width)) fail(`$.definitions.paints.${name}`, "must draw a fill or visible stroke");
504
+ });
505
+ let pathTotal = 0;
506
+ Object.entries(definitions.shapes).forEach(([name, entry]) => {
507
+ if (!entry.path.trim()) fail(`$.definitions.shapes.${name}.path`, "must not be blank");
508
+ pathTotal += entry.path.length;
509
+ });
510
+ if (pathTotal > LIMITS.pathTotal) fail("$.definitions.shapes", `paths must total at most ${LIMITS.pathTotal} characters`);
511
+ return descriptor;
512
+ };
513
+ var read = (value) => validate(readNode(schema, value, "$", /* @__PURE__ */ new WeakSet()));
514
+ var resolve = (segments) => {
515
+ let node = schema;
516
+ for (const segment of segments) {
517
+ while (node.kind === "nullable") node = node.item;
518
+ if (node.kind === "object" && Object.hasOwn(node.fields, segment)) node = node.fields[segment];
519
+ else if (node.kind === "record" && segment === "*") node = node.item;
520
+ else if (node.kind === "array" && segment === "[]") node = node.item;
521
+ else return null;
522
+ }
523
+ const nullable = node.kind === "nullable";
524
+ return {
525
+ node: nullable ? node.item : node,
526
+ nullable
527
+ };
528
+ };
529
+ var shapeOf = (node) => {
530
+ if (node.kind === "number") return {
531
+ kind: "number",
532
+ min: node.min,
533
+ max: node.max,
534
+ integer: node.integer
535
+ };
536
+ if (node.kind === "string") return {
537
+ kind: "string",
538
+ minLength: node.minLength,
539
+ maxLength: node.maxLength,
540
+ pattern: node.pattern && new RegExp(node.pattern.source, node.pattern.flags),
541
+ blankAllowed: node.blank !== false
542
+ };
543
+ if (node.kind === "choice") return {
544
+ kind: "choice",
545
+ values: Object.freeze([...node.values])
546
+ };
547
+ if (node.kind === "literal") return {
548
+ kind: "literal",
549
+ value: node.value
550
+ };
551
+ if (node.kind === "object") return {
552
+ kind: "object",
553
+ fields: Object.freeze(Object.keys(node.fields))
554
+ };
555
+ if (node.kind === "record") return {
556
+ kind: "record",
557
+ minEntries: node.minLength,
558
+ maxEntries: node.maxLength,
559
+ keyPattern: new RegExp(ID_PATTERN.source, ID_PATTERN.flags)
560
+ };
561
+ return {
562
+ kind: "array",
563
+ maxLength: node.maxLength
564
+ };
565
+ };
566
+ var bounds = (path) => {
567
+ if (typeof path !== "string" || !path) fail("$", "bounds path must be a non-empty string");
568
+ const found = resolve(path.replace(/\[\]/g, ".[]").split("."));
569
+ if (!found) fail(`$.${path}`, "is not a field in this schema");
570
+ return Object.freeze({
571
+ ...shapeOf(found.node),
572
+ nullable: found.nullable
573
+ });
574
+ };
575
+ var scaleNode = (node, value, factor) => {
576
+ if (node.kind === "nullable") return value === null ? null : scaleNode(node.item, value, factor);
577
+ if (node.kind === "number") return node.length ? value * factor ** node.length : value;
578
+ if (node.kind === "object") {
579
+ const out = {};
580
+ Object.keys(node.fields).forEach((key) => {
581
+ out[key] = scaleNode(node.fields[key], value[key], factor);
582
+ });
583
+ return out;
584
+ }
585
+ if (node.kind === "record") {
586
+ const out = {};
587
+ Object.keys(value).forEach((key) => {
588
+ out[key] = scaleNode(node.item, value[key], factor);
589
+ });
590
+ return out;
591
+ }
592
+ if (node.kind === "array") return value.map((item) => scaleNode(node.item, item, factor));
593
+ return value;
594
+ };
595
+ var scale = (descriptor, factor) => {
596
+ if (typeof factor !== "number" || !Number.isFinite(factor) || factor <= 0) fail("$", "scale factor must be a finite number greater than 0");
597
+ const source = read(descriptor);
598
+ const scaled = scaleNode(schema, source, factor);
599
+ const worldPaints = /* @__PURE__ */ new Set([source.chain.skin.ribbon.paint, source.legs.skin.limbPaint]);
600
+ const placements = [
601
+ scaled.legs.skin.foot,
602
+ ...scaled.chain.skin.plates,
603
+ ...scaled.chain.skin.ornaments
604
+ ];
605
+ const shapePaints = new Set(placements.map(({ paint }) => paint));
606
+ for (const id of shapePaints) {
607
+ const paint = source.definitions.paints[id];
608
+ if (!paint.stroke || paint.stroke.width === 0 || factor === 1) continue;
609
+ let shapeId = id;
610
+ if (worldPaints.has(id)) {
611
+ let suffix = 1;
612
+ do
613
+ shapeId = `shape-paint-${suffix++}`;
614
+ while (Object.hasOwn(scaled.definitions.paints, shapeId));
615
+ for (const placement of placements) if (placement.paint === id) placement.paint = shapeId;
616
+ }
617
+ scaled.definitions.paints[shapeId] = paint;
618
+ }
619
+ return read(scaled);
620
+ };
621
+ var parse = (text) => {
622
+ if (typeof text !== "string") fail("$", "JSON input must be a string");
623
+ let value;
624
+ try {
625
+ value = JSON.parse(text);
626
+ } catch (error) {
627
+ fail("$", `invalid JSON: ${error.message}`);
628
+ }
629
+ return read(value);
630
+ };
631
+ var stringify = (value, space = 2) => {
632
+ if (!Number.isInteger(space) || space < 0 || space > 10) fail("$", "indentation must be an integer from 0 to 10");
633
+ return JSON.stringify(read(value), null, space);
634
+ };
635
+
636
+ // src/model.mjs
637
+ /**
638
+ * Compiles a validated descriptor into immutable runtime topology. This is a
639
+ * private seam: callers keep the descriptor while physics consumes resolved
640
+ * chunks, links, sections, and visual placements.
641
+ */
642
+ var SECTION_NAMES = [
643
+ "head",
644
+ "trunk",
645
+ "tail"
646
+ ];
647
+ var MAX_BREATHING_STRAIN = .1;
648
+ var BREATHING_RATE_AT_16_CHUNKS = .15;
649
+ var MAX_BREATHING_RATE = .4;
650
+ var BREATH_CATCH_UP_PER_SECOND = 2 * MAX_BREATHING_STRAIN * 2 * Math.PI * MAX_BREATHING_RATE;
651
+ var LATERAL_SPACINGS_PER_SECOND = 14;
652
+ var MIN_SWING_RATE = .01;
653
+ var freeze = (value) => {
654
+ if (!value || typeof value !== "object" || Object.isFrozen(value)) return value;
655
+ Object.values(value).forEach(freeze);
656
+ return Object.freeze(value);
657
+ };
658
+ var lerp$1 = /* @__PURE__ */ __name((start, end, t) => start + (end - start) * t, "lerp");
659
+ var clamp$1 = /* @__PURE__ */ __name((value, low, high) => Math.max(low, Math.min(high, value)), "clamp");
660
+ var profileAt = (span, index, count) => count === 1 ? (span.start + span.end) / 2 : lerp$1(span.start, span.end, index / (count - 1));
661
+ var spatialAngle = (gait, channel, distance) => channel.phaseOffset - channel.harmonic * distance * gait.phaseLagRadiansPerPixel;
662
+ var breathingRateFor = (trunkChunks) => clamp$1(BREATHING_RATE_AT_16_CHUNKS * Math.sqrt(16 / trunkChunks), .1, MAX_BREATHING_RATE);
663
+ var placementChunks = (placement, sections, chunkCount) => {
664
+ const section = placement.at.section;
665
+ const scope = section ? sections[section] : {
666
+ start: 0,
667
+ count: chunkCount
668
+ };
669
+ const direction = placement.at.from === "head" ? 1 : -1;
670
+ const localStart = direction > 0 ? placement.at.offset : scope.count - 1 - placement.at.offset;
671
+ const available = Math.floor((scope.count - 1 - placement.at.offset) / placement.repeat.step) + 1;
672
+ const count = placement.repeat.count ?? available;
673
+ return Array.from({ length: count }, (_, index) => scope.start + localStart + direction * index * placement.repeat.step);
674
+ };
675
+ var normalizePaints = (paints) => {
676
+ const out = {};
677
+ Object.entries(paints).forEach(([paintId, paint]) => {
678
+ out[paintId] = {
679
+ fill: paint.fill,
680
+ stroke: paint.stroke ? paint.stroke.colour : null,
681
+ strokeWidth: paint.stroke ? paint.stroke.width : 0
682
+ };
683
+ });
684
+ return out;
685
+ };
686
+ var compile = (value) => {
687
+ const descriptor = read(value);
688
+ const gait = {
689
+ ...descriptor.gait,
690
+ bend: {
691
+ ...descriptor.gait.bend,
692
+ phaseOffset: 0
693
+ }
694
+ };
695
+ const sectionSpecs = descriptor.chain.sections;
696
+ const sections = {};
697
+ const chunks = [];
698
+ SECTION_NAMES.forEach((name) => {
699
+ const spec = sectionSpecs[name];
700
+ const start = chunks.length;
701
+ const material = descriptor.definitions.materials[spec.material];
702
+ for (let localIndex = 0; localIndex < spec.chunks; localIndex++) chunks.push({
703
+ index: chunks.length,
704
+ section: name,
705
+ localIndex,
706
+ restDistance: 0,
707
+ materialId: spec.material,
708
+ material,
709
+ motionScale: spec.motionScale,
710
+ bendScale: 0,
711
+ ribbonWidth: profileAt(spec.profile.ribbonWidth, localIndex, spec.chunks),
712
+ plateScale: profileAt(spec.profile.plateScale, localIndex, spec.chunks)
713
+ });
714
+ sections[name] = {
715
+ name,
716
+ start,
717
+ end: chunks.length,
718
+ count: spec.chunks,
719
+ spacing: spec.spacing,
720
+ materialId: spec.material,
721
+ material,
722
+ motionScale: spec.motionScale,
723
+ profile: spec.profile
724
+ };
725
+ });
726
+ const breathing = {
727
+ strain: descriptor.chain.breathing * MAX_BREATHING_STRAIN,
728
+ cyclesPerSecond: breathingRateFor(sections.trunk.count)
729
+ };
730
+ const links = [];
731
+ let restDistance = 0;
732
+ for (let index = 0; index < chunks.length - 1; index++) {
733
+ const before = chunks[index];
734
+ const after = chunks[index + 1];
735
+ const beforeSection = sections[before.section];
736
+ const afterSection = sections[after.section];
737
+ const restLength = before.section !== after.section ? (beforeSection.spacing + afterSection.spacing) / 2 : beforeSection.spacing;
738
+ const linkCorrection = (before.material.linkCorrection + after.material.linkCorrection) / 2;
739
+ restDistance += restLength;
740
+ after.restDistance = restDistance;
741
+ links.push({
742
+ index,
743
+ from: index,
744
+ to: index + 1,
745
+ restLength,
746
+ phaseDistance: (before.restDistance + after.restDistance) / 2,
747
+ linkCorrection,
748
+ gatherScale: (before.motionScale.gather + after.motionScale.gather) / 2,
749
+ breathingScale: before.section === "trunk" && after.section === "trunk" ? breathing.strain : 0
750
+ });
751
+ }
752
+ for (let index = 1; index < chunks.length - 1; index++) chunks[index].bendScale = (links[index - 1].restLength + links[index].restLength) / 2 / sections.trunk.spacing;
753
+ chunks.forEach((chunk) => {
754
+ const angle = spatialAngle(gait, gait.bend, chunk.restDistance);
755
+ chunk.bendPhaseSine = Math.sin(angle);
756
+ chunk.bendPhaseCosine = Math.cos(angle);
757
+ });
758
+ links.forEach((link) => {
759
+ const angle = spatialAngle(gait, gait.gather, link.phaseDistance);
760
+ link.gatherPhaseSine = Math.sin(angle);
761
+ link.gatherPhaseCosine = Math.cos(angle);
762
+ });
763
+ const shapes = descriptor.definitions.shapes;
764
+ const paints = normalizePaints(descriptor.definitions.paints);
765
+ const plates = descriptor.chain.skin.plates.flatMap((placement) => placementChunks(placement, sections, chunks.length).map((chunk) => ({
766
+ id: placement.id,
767
+ chunk,
768
+ shape: shapes[placement.shape],
769
+ paint: paints[placement.paint],
770
+ scale: placement.scale
771
+ })));
772
+ const ornaments = descriptor.chain.skin.ornaments.flatMap((placement) => {
773
+ const sides = placement.side === "both" ? ["left", "right"] : [placement.side];
774
+ return placementChunks(placement, sections, chunks.length).flatMap((chunk) => sides.map((side) => {
775
+ const sideSign = side === "left" ? -1 : 1;
776
+ const angle = placement.angleDegrees * Math.PI * sideSign / 180;
777
+ return {
778
+ id: placement.id,
779
+ chunk,
780
+ side,
781
+ sideSign,
782
+ layer: placement.layer,
783
+ shape: shapes[placement.shape],
784
+ paint: paints[placement.paint],
785
+ offset: placement.offset,
786
+ angleDegrees: placement.angleDegrees,
787
+ angleCosine: Math.cos(angle),
788
+ angleSine: Math.sin(angle),
789
+ scale: placement.scale,
790
+ waveGain: placement.react * (1 - placement.source),
791
+ physGain: placement.react * placement.source,
792
+ recover: placement.recover,
793
+ wobble: placement.wobble
794
+ };
795
+ }));
796
+ });
797
+ return freeze({
798
+ descriptor,
799
+ sections,
800
+ chunks,
801
+ links,
802
+ restLength: restDistance,
803
+ gait,
804
+ paints,
805
+ physics: descriptor.chain.physics,
806
+ breathing,
807
+ skin: {
808
+ lateralRate: LATERAL_SPACINGS_PER_SECOND * sections.trunk.spacing,
809
+ loadScale: descriptor.chain.skin.loadScale,
810
+ hasRibbon: chunks.some((chunk) => chunk.ribbonWidth > 0),
811
+ ribbonPaintId: descriptor.chain.skin.ribbon.paint,
812
+ ribbonPaint: paints[descriptor.chain.skin.ribbon.paint],
813
+ plates,
814
+ platesTailFirst: [...plates].sort((before, after) => after.chunk - before.chunk),
815
+ ornaments
816
+ },
817
+ legs: {
818
+ ...descriptor.legs,
819
+ spread: descriptor.legs.spread * descriptor.legs.reach,
820
+ swingArc: descriptor.legs.swingArc * descriptor.legs.reach,
821
+ swingSeconds: descriptor.legs.swingCycles / Math.max(descriptor.gait.cyclesPerSecond, MIN_SWING_RATE),
822
+ start: sections[descriptor.legs.section].start,
823
+ end: sections[descriptor.legs.section].end,
824
+ skin: {
825
+ limbPaint: paints[descriptor.legs.skin.limbPaint],
826
+ limbWidth: descriptor.legs.skin.limbWidth,
827
+ foot: {
828
+ shape: shapes[descriptor.legs.skin.foot.shape],
829
+ paint: paints[descriptor.legs.skin.foot.paint],
830
+ scale: descriptor.legs.skin.foot.scale,
831
+ plantedScale: descriptor.legs.skin.foot.plantedScale
832
+ }
833
+ }
834
+ }
835
+ });
836
+ };
837
+
838
+ // src/drive.mjs
839
+ /** Schema-v1 gait clock and spatial channels. Private to the Beefwife runtime. */
840
+ var TAU$2 = Math.PI * 2;
841
+ var positiveModulo = (value, divisor) => (value % divisor + divisor) % divisor;
842
+ var Gait = class {
843
+ constructor(gait, phase = 0) {
844
+ this.gait = gait;
845
+ this.phase = positiveModulo(phase, TAU$2);
846
+ }
847
+ advance(dt, throttle) {
848
+ this.phase = positiveModulo(this.phase + TAU$2 * this.gait.cyclesPerSecond * throttle * dt, TAU$2);
849
+ }
850
+ _phaseAt(distance, channel, phaseOffset = 0) {
851
+ return channel.harmonic * (this.phase - distance * this.gait.phaseLagRadiansPerPixel) + channel.phaseOffset + phaseOffset;
852
+ }
853
+ _pulseAt(distance, channel, phaseOffset = 0) {
854
+ const cycle = positiveModulo(this._phaseAt(distance, channel, phaseOffset), TAU$2) / TAU$2;
855
+ if (cycle >= channel.dutyCycle) return 0;
856
+ return Math.sin(Math.PI * cycle / channel.dutyCycle);
857
+ }
858
+ bendAt(distance, throttle, scale) {
859
+ const channel = this.gait.bend;
860
+ return channel.amplitude * scale * throttle * Math.sin(this._phaseAt(distance, channel));
861
+ }
862
+ thrustAt(distance, throttle, scale) {
863
+ const channel = this.gait.thrust;
864
+ return channel.acceleration * scale * throttle * this._pulseAt(distance, channel);
865
+ }
866
+ restAt(distance, throttle, scale) {
867
+ const channel = this.gait.gather;
868
+ return 1 + channel.amplitude * scale * throttle * Math.cos(this._phaseAt(distance, channel));
869
+ }
870
+ contactAt(distance, throttle, scale, phaseOffset = 0) {
871
+ const channel = this.gait.contact;
872
+ return 1 - channel.amplitude * scale * throttle * this._pulseAt(distance, channel, phaseOffset);
873
+ }
874
+ };
875
+
876
+ // src/tables.mjs
877
+ /**
878
+ * The model, flattened for the solver. Every per-chunk and per-link constant
879
+ * a substep reads, unpacked from the model's nested records into parallel
880
+ * typed arrays: the substep runs over every chunk many times a second, and a
881
+ * frozen `spec.material.grip.forward` is four loads where an array index is
882
+ * one. Nothing here is state; rebuilding it from the same model, gait and
883
+ * substep gives the same numbers.
884
+ */
885
+ var REFERENCE_SUBSTEP_RATE = 60;
886
+ var REFERENCE_LINK_SOLVE_RATE = 480;
887
+ var shareAtRate = (share, rate, referenceRate) => {
888
+ if (rate === referenceRate) return share;
889
+ if (!(share > 0)) return 0;
890
+ if (share >= 1) return 1;
891
+ return 1 - Math.pow(1 - share, referenceRate / rate);
892
+ };
893
+ var ChainTables = class {
894
+ constructor(model, gait, substep, relaxPasses) {
895
+ const count = model.chunks.length;
896
+ const linkCount = model.links.length;
897
+ this.retention = new Float64Array(count);
898
+ this.gripForward = new Float64Array(count);
899
+ this.gripBackward = new Float64Array(count);
900
+ this.gripLateral = new Float64Array(count);
901
+ this.motionThrust = new Float64Array(count);
902
+ this.motionContact = new Float64Array(count);
903
+ this.motionBend = new Float64Array(count);
904
+ this.bendScale = new Float64Array(count);
905
+ this.bendPhaseSine = new Float64Array(count);
906
+ this.bendPhaseCosine = new Float64Array(count);
907
+ this.jointCorrectionHalf = new Float64Array(count);
908
+ this.phaseLag = new Float64Array(count);
909
+ this.linkRestLength = new Float64Array(linkCount);
910
+ this.linkCorrectionHalf = new Float64Array(linkCount);
911
+ this.gatherScale = new Float64Array(linkCount);
912
+ this.gatherPhaseSine = new Float64Array(linkCount);
913
+ this.gatherPhaseCosine = new Float64Array(linkCount);
914
+ this.linkBreathes = new Uint8Array(linkCount);
915
+ this.refresh(model, gait, substep, relaxPasses);
916
+ }
917
+ refresh(model, gait, substep, relaxPasses) {
918
+ const lag = gait.phaseLagRadiansPerPixel;
919
+ const substepRate = 1 / substep;
920
+ const linkSolveRate = relaxPasses / substep;
921
+ const perSubstep = (share) => shareAtRate(share, substepRate, REFERENCE_SUBSTEP_RATE);
922
+ for (let index = 0; index < model.chunks.length; index++) {
923
+ const spec = model.chunks[index];
924
+ const grip = spec.material.grip;
925
+ this.retention[index] = Math.pow(spec.material.velocityRetention, substep);
926
+ this.gripForward[index] = perSubstep(grip.forward);
927
+ this.gripBackward[index] = perSubstep(grip.backward);
928
+ this.gripLateral[index] = perSubstep(grip.lateral);
929
+ this.motionThrust[index] = spec.motionScale.thrust;
930
+ this.motionContact[index] = spec.motionScale.contact;
931
+ this.motionBend[index] = spec.motionScale.bend;
932
+ this.bendScale[index] = spec.bendScale;
933
+ this.bendPhaseSine[index] = spec.bendPhaseSine;
934
+ this.bendPhaseCosine[index] = spec.bendPhaseCosine;
935
+ this.jointCorrectionHalf[index] = perSubstep(spec.material.jointCorrection) * .5;
936
+ this.phaseLag[index] = spec.restDistance * lag;
937
+ }
938
+ for (let index = 0; index < model.links.length; index++) {
939
+ const link = model.links[index];
940
+ this.linkRestLength[index] = link.restLength;
941
+ this.linkCorrectionHalf[index] = shareAtRate(link.linkCorrection, linkSolveRate, REFERENCE_LINK_SOLVE_RATE) * .5;
942
+ this.gatherScale[index] = link.gatherScale;
943
+ this.gatherPhaseSine[index] = link.gatherPhaseSine;
944
+ this.gatherPhaseCosine[index] = link.gatherPhaseCosine;
945
+ this.linkBreathes[index] = link.breathingScale ? 1 : 0;
946
+ }
947
+ }
948
+ };
949
+
950
+ // src/chain.mjs
951
+ /**
952
+ * The chain's mutable state, one typed array per field. The counterpart to
953
+ * ChainTables: that holds what the model says and never changes, this holds
954
+ * what the solver moves. A substep sweeps every chunk several times over, and
955
+ * a run of positions the CPU can prefetch is worth more than a record that
956
+ * keeps each chunk's ten fields together.
957
+ *
958
+ * Also the operations that move or measure the whole chain in the world, which
959
+ * are not solver steps and read nothing but the chain itself.
960
+ */
961
+ var ChainState = class {
962
+ constructor(count) {
963
+ this.count = count;
964
+ this.x = new Float64Array(count);
965
+ this.y = new Float64Array(count);
966
+ this.px = new Float64Array(count);
967
+ this.py = new Float64Array(count);
968
+ this.dx = new Float64Array(count);
969
+ this.dy = new Float64Array(count);
970
+ this.idle = new Float64Array(count);
971
+ this.gain = new Float64Array(count);
972
+ this.gaitContact = new Float64Array(count);
973
+ this.contact = new Float64Array(count);
974
+ this.correction = {
975
+ x: 0,
976
+ y: 0
977
+ };
978
+ this.dx.fill(1);
979
+ this.gaitContact.fill(1);
980
+ this.contact.fill(1);
981
+ }
982
+ translate(offset) {
983
+ const { x, y, px, py, count } = this;
984
+ for (let index = 0; index < count; index++) {
985
+ x[index] += offset.x;
986
+ y[index] += offset.y;
987
+ px[index] += offset.x;
988
+ py[index] += offset.y;
989
+ }
990
+ }
991
+ fitsTranslation(offset, limit) {
992
+ const { x, y, px, py, count } = this;
993
+ const within = (value, shift) => {
994
+ const next = value + shift;
995
+ return Number.isFinite(next) && Math.abs(next) <= limit;
996
+ };
997
+ for (let index = 0; index < count; index++) if (!within(x[index], offset.x) || !within(y[index], offset.y) || !within(px[index], offset.x) || !within(py[index], offset.y)) return false;
998
+ return true;
999
+ }
1000
+ worldCorrection(limit) {
1001
+ const { x, y, px, py, count } = this;
1002
+ let minimumX = 1e400;
1003
+ let maximumX = -1e400;
1004
+ let minimumY = 1e400;
1005
+ let maximumY = -1e400;
1006
+ for (let index = 0; index < count; index++) {
1007
+ minimumX = Math.min(minimumX, x[index], px[index]);
1008
+ maximumX = Math.max(maximumX, x[index], px[index]);
1009
+ minimumY = Math.min(minimumY, y[index], py[index]);
1010
+ maximumY = Math.max(maximumY, y[index], py[index]);
1011
+ }
1012
+ const correction = (minimum, maximum) => {
1013
+ if (maximum > limit) return limit - maximum;
1014
+ if (minimum < -limit) return -limit - minimum;
1015
+ return 0;
1016
+ };
1017
+ this.correction.x = correction(minimumX, maximumX);
1018
+ this.correction.y = correction(minimumY, maximumY);
1019
+ return this.correction;
1020
+ }
1021
+ };
1022
+
1023
+ // src/carry.mjs
1024
+ /**
1025
+ * Moving chunk state onto a chain whose section counts changed. A chunk the
1026
+ * descriptor still names keeps its position and velocity; an added one is
1027
+ * seeded from its neighbours. The creature settles from where it was rather
1028
+ * than snapping straight, and since head always holds a chunk the new chain
1029
+ * always has something to carry from.
1030
+ */
1031
+ var nameOf = (spec) => `${spec.section}:${spec.localIndex}`;
1032
+ var FIELDS = [
1033
+ "x",
1034
+ "y",
1035
+ "px",
1036
+ "py",
1037
+ "dx",
1038
+ "dy",
1039
+ "idle",
1040
+ "gain"
1041
+ ];
1042
+ var carryChunks = (chain, model, previousChain, previousModel) => {
1043
+ const source = /* @__PURE__ */ new Map();
1044
+ previousModel.chunks.forEach((spec, index) => source.set(nameOf(spec), index));
1045
+ const carried = model.chunks.map((spec, index) => {
1046
+ const from = source.get(nameOf(spec));
1047
+ if (from === void 0) return false;
1048
+ for (const field of FIELDS) chain[field][index] = previousChain[field][from];
1049
+ return true;
1050
+ });
1051
+ const { x, y, px, py, dx, dy, idle, gain, count } = chain;
1052
+ for (let index = 0; index < count; index++) {
1053
+ if (carried[index]) continue;
1054
+ let before = index - 1;
1055
+ while (before >= 0 && !carried[before]) before--;
1056
+ let after = index + 1;
1057
+ while (after < count && !carried[after]) after++;
1058
+ if (before >= 0 && after < count) {
1059
+ const along = (index - before) / (after - before);
1060
+ x[index] = x[before] + (x[after] - x[before]) * along;
1061
+ y[index] = y[before] + (y[after] - y[before]) * along;
1062
+ px[index] = px[before] + (px[after] - px[before]) * along;
1063
+ py[index] = py[before] + (py[after] - py[before]) * along;
1064
+ dx[index] = dx[before];
1065
+ dy[index] = dy[before];
1066
+ } else {
1067
+ const anchor = before >= 0 ? before : after;
1068
+ const heading = before >= 0 ? -1 : 1;
1069
+ const link = model.links[before >= 0 ? index - 1 : index];
1070
+ const reach = (link ? link.restLength : 0) * Math.abs(index - anchor);
1071
+ x[index] = x[anchor] + dx[anchor] * heading * reach;
1072
+ y[index] = y[anchor] + dy[anchor] * heading * reach;
1073
+ px[index] = x[index];
1074
+ py[index] = y[index];
1075
+ dx[index] = dx[anchor];
1076
+ dy[index] = dy[anchor];
1077
+ }
1078
+ idle[index] = 0;
1079
+ gain[index] = 0;
1080
+ }
1081
+ };
1082
+
1083
+ // src/bend.mjs
1084
+ /**
1085
+ * The chain's angular constraint. One joint on its own reaches the turn it is
1086
+ * asked for exactly, but a chain of them barely bends: each joint pushes its
1087
+ * outer chunks one way and its middle the other, so a smooth run of targets
1088
+ * makes a smooth displacement field, and a joint angle is the second
1089
+ * difference of that field. Sweeping wider spans first gives the solver a
1090
+ * curvature a single joint cannot see, and the single joints then finish.
1091
+ *
1092
+ * Nothing here is state between substeps. The arrays are scratch, sized once
1093
+ * so a substep allocates nothing.
1094
+ */
1095
+ var MAX_BEND_SPAN = 2;
1096
+ var Bend = class {
1097
+ constructor(count) {
1098
+ this.targets = new Float64Array(count);
1099
+ this.wantedX = new Float64Array(count);
1100
+ this.wantedY = new Float64Array(count);
1101
+ this.spanX = new Float64Array(count);
1102
+ this.spanY = new Float64Array(count);
1103
+ }
1104
+ update(model, gait, tables, restLengths, throttle, bias) {
1105
+ const channel = model.gait.bend;
1106
+ const phase = channel.harmonic * gait.phase;
1107
+ const phaseSine = Math.sin(phase);
1108
+ const phaseCosine = Math.cos(phase);
1109
+ const amplitude = channel.amplitude;
1110
+ const biasThrottle = bias * throttle;
1111
+ const { motionBend, bendPhaseSine, bendPhaseCosine, bendScale } = tables;
1112
+ const targets = this.targets;
1113
+ const wantedX = this.wantedX;
1114
+ const wantedY = this.wantedY;
1115
+ const last = targets.length - 1;
1116
+ let heading = 0;
1117
+ wantedX[0] = 0;
1118
+ wantedY[0] = 0;
1119
+ for (let index = 0; index < last; index++) {
1120
+ if (index > 0) {
1121
+ const bend = amplitude * motionBend[index] * throttle * (phaseSine * bendPhaseCosine[index] + phaseCosine * bendPhaseSine[index]);
1122
+ targets[index] = (bend + biasThrottle) * bendScale[index];
1123
+ heading += targets[index];
1124
+ }
1125
+ wantedX[index + 1] = wantedX[index] + Math.cos(heading) * restLengths[index];
1126
+ wantedY[index + 1] = wantedY[index] + Math.sin(heading) * restLengths[index];
1127
+ }
1128
+ }
1129
+ relax(chain, jointCorrectionHalf) {
1130
+ for (let span = Math.max(1, Math.min(2, chain.count >> 2)); span >= 1; span >>= 1) this.relaxSpan(chain, jointCorrectionHalf, span);
1131
+ }
1132
+ relaxSpan(chain, jointCorrectionHalf, span) {
1133
+ const count = chain.count;
1134
+ const chunkX = chain.x;
1135
+ const chunkY = chain.y;
1136
+ const wantedX = this.wantedX;
1137
+ const wantedY = this.wantedY;
1138
+ const nextX = this.spanX;
1139
+ const nextY = this.spanY;
1140
+ for (let pivot = span; pivot + span < count; pivot += span) {
1141
+ const from = pivot - span;
1142
+ const to = pivot + span;
1143
+ const pivotX = chunkX[pivot];
1144
+ const pivotY = chunkY[pivot];
1145
+ const ax = pivotX - chunkX[from];
1146
+ const ay = pivotY - chunkY[from];
1147
+ const bx = chunkX[to] - pivotX;
1148
+ const by = chunkY[to] - pivotY;
1149
+ const turn = Math.atan2(ax * by - ay * bx, ax * bx + ay * by);
1150
+ const wx = wantedX[pivot] - wantedX[from];
1151
+ const wy = wantedY[pivot] - wantedY[from];
1152
+ const vx = wantedX[to] - wantedX[pivot];
1153
+ const vy = wantedY[to] - wantedY[pivot];
1154
+ const correction = (Math.atan2(wx * vy - wy * vx, wx * vx + wy * vy) - turn) * jointCorrectionHalf[pivot];
1155
+ const cosine = Math.cos(correction);
1156
+ const sine = Math.sin(correction);
1157
+ for (let index = from; index <= to; index++) {
1158
+ const x = chunkX[index] - pivotX;
1159
+ const y = chunkY[index] - pivotY;
1160
+ const away = index < pivot ? -sine : sine;
1161
+ nextX[index] = pivotX + x * cosine - y * away;
1162
+ nextY[index] = pivotY + x * away + y * cosine;
1163
+ }
1164
+ const width = to - from + 1;
1165
+ let driftX = 0;
1166
+ let driftY = 0;
1167
+ let centerX = 0;
1168
+ let centerY = 0;
1169
+ for (let index = from; index <= to; index++) {
1170
+ driftX += nextX[index] - chunkX[index];
1171
+ driftY += nextY[index] - chunkY[index];
1172
+ centerX += chunkX[index];
1173
+ centerY += chunkY[index];
1174
+ }
1175
+ driftX /= width;
1176
+ driftY /= width;
1177
+ centerX /= width;
1178
+ centerY /= width;
1179
+ let moment = 0;
1180
+ let inertia = 0;
1181
+ for (let index = from; index <= to; index++) {
1182
+ const rx = chunkX[index] - centerX;
1183
+ const ry = chunkY[index] - centerY;
1184
+ moment += rx * (nextY[index] - chunkY[index] - driftY) - ry * (nextX[index] - chunkX[index] - driftX);
1185
+ inertia += rx * rx + ry * ry;
1186
+ }
1187
+ const spin = inertia > 1e-12 ? moment / inertia : 0;
1188
+ for (let index = from; index <= to; index++) {
1189
+ const rx = chunkX[index] - centerX;
1190
+ const ry = chunkY[index] - centerY;
1191
+ chunkX[index] = nextX[index] - driftX + spin * ry;
1192
+ chunkY[index] = nextY[index] - driftY - spin * rx;
1193
+ }
1194
+ }
1195
+ }
1196
+ response(chain, into = []) {
1197
+ const targets = this.targets;
1198
+ const { x, y, count } = chain;
1199
+ into.length = 0;
1200
+ for (let index = 1; index < count - 1; index++) {
1201
+ const ax = x[index] - x[index - 1];
1202
+ const ay = y[index] - y[index - 1];
1203
+ const bx = x[index + 1] - x[index];
1204
+ const by = y[index + 1] - y[index];
1205
+ into.push({
1206
+ joint: index,
1207
+ commanded: targets[index],
1208
+ delivered: Math.atan2(ax * by - ay * bx, ax * bx + ay * by)
1209
+ });
1210
+ }
1211
+ return into;
1212
+ }
1213
+ };
1214
+
1215
+ // src/body.mjs
1216
+ /** Schema-v1 Verlet chain. Private state is owned by one Beefwife instance. */
1217
+ var TAU$1 = Math.PI * 2;
1218
+ var RELAX_PASSES = 8;
1219
+ var AXIS_RATE = 1.5;
1220
+ var MAX_LINK_STRETCH = 3;
1221
+ var magnitude$2 = /* @__PURE__ */ __name((x, y) => Math.sqrt(x * x + y * y), "magnitude");
1222
+ var compareGain = (gain, before, after) => gain[before] - gain[after] || before - after;
1223
+ var selectLowest = (order, gain, count) => {
1224
+ if (count <= 0 || count >= order.length) return;
1225
+ const target = count - 1;
1226
+ let left = 0;
1227
+ let right = order.length - 1;
1228
+ while (left < right) {
1229
+ const pivot = order[left + right >> 1];
1230
+ let lower = left;
1231
+ let upper = right;
1232
+ while (lower <= upper) {
1233
+ while (lower <= right && compareGain(gain, order[lower], pivot) < 0) lower++;
1234
+ while (upper >= left && compareGain(gain, pivot, order[upper]) < 0) upper--;
1235
+ if (lower <= upper) {
1236
+ const swap = order[lower];
1237
+ order[lower++] = order[upper];
1238
+ order[upper--] = swap;
1239
+ }
1240
+ }
1241
+ if (target <= upper) right = upper;
1242
+ else if (target >= lower) left = lower;
1243
+ else return;
1244
+ }
1245
+ };
1246
+ var Body = class {
1247
+ constructor(model, gait, breathingPhase = gait.phase) {
1248
+ this.model = model;
1249
+ this.gait = gait;
1250
+ this.breathingPhase = breathingPhase;
1251
+ this.breathingScale = 0;
1252
+ this.accumulator = 0;
1253
+ this.axis = {
1254
+ x: 1,
1255
+ y: 0
1256
+ };
1257
+ this.steeringBias = 0;
1258
+ this.chain = new ChainState(model.chunks.length);
1259
+ this.linkTargets = new Float64Array(model.links.length);
1260
+ this.bend = new Bend(model.chunks.length);
1261
+ this.breathingShiftX = new Float64Array(model.chunks.length);
1262
+ this.breathingShiftY = new Float64Array(model.chunks.length);
1263
+ this.liftOrder = model.chunks.map((_, index) => index);
1264
+ this.liftTargets = new Float64Array(model.chunks.length);
1265
+ this.tables = new ChainTables(model, model.gait, PHYSICS_STEP, RELAX_PASSES);
1266
+ }
1267
+ reconfigure(model, gait, throttle = 1, breathingPhase = this.breathingPhase) {
1268
+ if (model.chunks.length !== this.chain.count) throw new Error("cannot reconfigure a different chunk count");
1269
+ this.model = model;
1270
+ this.gait = gait;
1271
+ this.breathingPhase = breathingPhase;
1272
+ this.tables.refresh(model, model.gait, PHYSICS_STEP, RELAX_PASSES);
1273
+ this.refreshContacts(throttle);
1274
+ }
1275
+ place(position, direction) {
1276
+ const { x, y, px, py, dx, dy, idle, gain, count } = this.chain;
1277
+ for (let index = 0; index < count; index++) {
1278
+ const distance = this.model.chunks[index].restDistance;
1279
+ x[index] = position.x - direction.x * distance;
1280
+ y[index] = position.y - direction.y * distance;
1281
+ px[index] = x[index];
1282
+ py[index] = y[index];
1283
+ dx[index] = direction.x;
1284
+ dy[index] = direction.y;
1285
+ idle[index] = 0;
1286
+ gain[index] = 0;
1287
+ }
1288
+ this.axis = { ...direction };
1289
+ this.steeringBias = 0;
1290
+ this.accumulator = 0;
1291
+ this.breathingScale = 0;
1292
+ this._breathe(this._breathingTarget());
1293
+ const offsetX = position.x - x[0];
1294
+ const offsetY = position.y - y[0];
1295
+ for (let index = 0; index < count; index++) {
1296
+ x[index] += offsetX;
1297
+ y[index] += offsetY;
1298
+ px[index] += offsetX;
1299
+ py[index] += offsetY;
1300
+ }
1301
+ this.refreshContacts(1);
1302
+ }
1303
+ adopt(previous) {
1304
+ carryChunks(this.chain, this.model, previous.chain, previous.model);
1305
+ this.axis = { ...previous.axis };
1306
+ this.steeringBias = previous.steeringBias;
1307
+ this.accumulator = previous.accumulator;
1308
+ this.breathingScale = previous.breathingScale;
1309
+ }
1310
+ refreshContacts(throttle) {
1311
+ const autoLift = this.model.physics.autoLift;
1312
+ const { idle, gaitContact, contact, count } = this.chain;
1313
+ for (let index = 0; index < count; index++) {
1314
+ const spec = this.model.chunks[index];
1315
+ gaitContact[index] = this.gait.contactAt(spec.restDistance, throttle, spec.motionScale.contact);
1316
+ contact[index] = Math.max(0, Math.min(1, gaitContact[index] * (1 - autoLift.amount * idle[index] * throttle)));
1317
+ }
1318
+ }
1319
+ step(dt, throttle, direction, afterSubstep) {
1320
+ const elapsed = this.accumulator + dt;
1321
+ const tolerance = Number.EPSILON * Math.max(1, elapsed);
1322
+ const due = Math.floor((elapsed + tolerance) / PHYSICS_STEP);
1323
+ this.accumulator = elapsed;
1324
+ for (let consumed = 1; consumed <= due; consumed++) {
1325
+ this.accumulator = Math.max(0, elapsed - consumed * PHYSICS_STEP);
1326
+ this._substep(PHYSICS_STEP, throttle, direction);
1327
+ if (afterSubstep) afterSubstep(PHYSICS_STEP);
1328
+ }
1329
+ return due > 0;
1330
+ }
1331
+ _substep(dt, throttle, direction) {
1332
+ this.gait.advance(dt, throttle);
1333
+ this.breathingPhase = (this.breathingPhase + TAU$1 * this.model.breathing.cyclesPerSecond * dt) % TAU$1;
1334
+ this._updateTangentsAndAxis(dt);
1335
+ const limit = BREATH_CATCH_UP_PER_SECOND * dt;
1336
+ const change = this._breathingTarget() - this.breathingScale;
1337
+ this._breathe(this.breathingScale + Math.max(-limit, Math.min(limit, change)));
1338
+ this._integrate(dt, throttle);
1339
+ this._updateLinkTargets(throttle);
1340
+ this.bend.update(this.model, this.gait, this.tables, this.linkTargets, throttle, this._steer(direction, dt));
1341
+ this.bend.relax(this.chain, this.tables.jointCorrectionHalf);
1342
+ for (let pass = 0; pass < RELAX_PASSES; pass++) this._relaxLinks();
1343
+ this._clampLinks();
1344
+ this._applyAutoLift(dt, throttle);
1345
+ }
1346
+ _breathingTarget() {
1347
+ return this.model.breathing.strain * Math.sin(this.breathingPhase);
1348
+ }
1349
+ _breathe(nextScale) {
1350
+ const scaleChange = nextScale - this.breathingScale;
1351
+ this.breathingScale = nextScale;
1352
+ if (Math.abs(scaleChange) < 1e-15) return;
1353
+ const { start, end, count, spacing } = this.model.sections.trunk;
1354
+ const middle = (count - 1) / 2;
1355
+ const { x, y, px, py, dx, dy, count: chunkCount } = this.chain;
1356
+ let meanX = 0;
1357
+ let meanY = 0;
1358
+ for (let index = 0; index < chunkCount; index++) {
1359
+ const position = index < start ? middle : index >= end ? -middle : middle - (index - start);
1360
+ const tangent = index < start ? start : index >= end ? end - 1 : index;
1361
+ const distance = position * spacing * scaleChange;
1362
+ const shiftX = dx[tangent] * distance;
1363
+ const shiftY = dy[tangent] * distance;
1364
+ this.breathingShiftX[index] = shiftX;
1365
+ this.breathingShiftY[index] = shiftY;
1366
+ meanX += shiftX / chunkCount;
1367
+ meanY += shiftY / chunkCount;
1368
+ }
1369
+ for (let index = 0; index < chunkCount; index++) {
1370
+ const shiftX = this.breathingShiftX[index] - meanX;
1371
+ const shiftY = this.breathingShiftY[index] - meanY;
1372
+ x[index] += shiftX;
1373
+ y[index] += shiftY;
1374
+ px[index] += shiftX;
1375
+ py[index] += shiftY;
1376
+ }
1377
+ }
1378
+ _updateTangentsAndAxis(dt) {
1379
+ const { x, y, px, py, dx, dy, count } = this.chain;
1380
+ const last = count - 1;
1381
+ let axisX = 0;
1382
+ let axisY = 0;
1383
+ for (let index = 0; index < count; index++) {
1384
+ const ahead = index === 0 ? 0 : index - 1;
1385
+ const behind = index === last ? last : index + 1;
1386
+ const spanX = x[ahead] - x[behind];
1387
+ const spanY = y[ahead] - y[behind];
1388
+ const tangentLength = Math.sqrt(spanX * spanX + spanY * spanY);
1389
+ if (tangentLength >= 1e-9) {
1390
+ dx[index] = spanX / tangentLength;
1391
+ dy[index] = spanY / tangentLength;
1392
+ }
1393
+ axisX += x[index] - px[index];
1394
+ axisY += y[index] - py[index];
1395
+ }
1396
+ const axisLength = Math.sqrt(axisX * axisX + axisY * axisY);
1397
+ if (axisLength < 1e-9) return;
1398
+ const amount = Math.min(1, dt * AXIS_RATE);
1399
+ this.axis.x += (axisX / axisLength - this.axis.x) * amount;
1400
+ this.axis.y += (axisY / axisLength - this.axis.y) * amount;
1401
+ const length = magnitude$2(this.axis.x, this.axis.y) || 1;
1402
+ this.axis.x /= length;
1403
+ this.axis.y /= length;
1404
+ }
1405
+ _integrate(dt, throttle) {
1406
+ const dtSquared = dt * dt;
1407
+ const gait = this.gait.gait;
1408
+ const phase = this.gait.phase;
1409
+ const contactChannel = gait.contact;
1410
+ const thrust = gait.thrust;
1411
+ const contactHarmonic = contactChannel.harmonic;
1412
+ const thrustHarmonic = thrust.harmonic;
1413
+ const contactPhaseOffset = contactChannel.phaseOffset;
1414
+ const thrustPhaseOffset = thrust.phaseOffset;
1415
+ const contactDuty = contactChannel.dutyCycle;
1416
+ const thrustDuty = thrust.dutyCycle;
1417
+ const contactAmplitude = contactChannel.amplitude;
1418
+ const thrustAcceleration = thrust.acceleration;
1419
+ const autoLift = this.model.physics.autoLift.amount;
1420
+ const axisX = this.axis.x;
1421
+ const axisY = this.axis.y;
1422
+ const { retention, gripForward, gripBackward, gripLateral, motionContact, motionThrust, phaseLag } = this.tables;
1423
+ const { x, y, px, py, dx, dy, idle, gain, gaitContact, contact, count } = this.chain;
1424
+ for (let index = 0; index < count; index++) {
1425
+ const hold = retention[index];
1426
+ const startX = x[index];
1427
+ const startY = y[index];
1428
+ const velocityX = (startX - px[index]) * hold;
1429
+ const velocityY = (startY - py[index]) * hold;
1430
+ px[index] = startX;
1431
+ py[index] = startY;
1432
+ const movedX = startX + velocityX;
1433
+ const movedY = startY + velocityY;
1434
+ const tangentX = dx[index];
1435
+ const tangentY = dy[index];
1436
+ const spanX = movedX - startX;
1437
+ const spanY = movedY - startY;
1438
+ const along = spanX * tangentX + spanY * tangentY;
1439
+ const lateral = spanX * -tangentY + spanY * tangentX;
1440
+ const lagged = phase - phaseLag[index];
1441
+ const contactCycle = positiveModulo(contactHarmonic * lagged + contactPhaseOffset, TAU$1) / TAU$1;
1442
+ const cycleContact = contactCycle >= contactDuty ? 1 : 1 - contactAmplitude * motionContact[index] * throttle * Math.sin(Math.PI * contactCycle / contactDuty);
1443
+ gaitContact[index] = cycleContact;
1444
+ const grounded = Math.max(0, Math.min(1, cycleContact * (1 - autoLift * idle[index] * throttle)));
1445
+ contact[index] = grounded;
1446
+ const retainedAlong = along * (1 - grounded * (along < 0 ? gripBackward[index] : gripForward[index]));
1447
+ const retainedLateral = lateral * (1 - grounded * gripLateral[index]);
1448
+ const heldX = startX + tangentX * retainedAlong - tangentY * retainedLateral;
1449
+ const heldY = startY + tangentY * retainedAlong + tangentX * retainedLateral;
1450
+ gain[index] = -((along - retainedAlong) * (tangentX * axisX + tangentY * axisY) + (lateral - retainedLateral) * (-tangentY * axisX + tangentX * axisY));
1451
+ const thrustCycle = positiveModulo(thrustHarmonic * lagged + thrustPhaseOffset, TAU$1) / TAU$1;
1452
+ const acceleration = thrustCycle >= thrustDuty ? 0 : thrustAcceleration * motionThrust[index] * throttle * Math.sin(Math.PI * thrustCycle / thrustDuty);
1453
+ x[index] = heldX + tangentX * acceleration * dtSquared;
1454
+ y[index] = heldY + tangentY * acceleration * dtSquared;
1455
+ }
1456
+ }
1457
+ _steer(direction, dt) {
1458
+ const steering = this.model.physics.steering;
1459
+ const { dx, dy } = this.chain;
1460
+ const error = Math.atan2(dx[0] * direction.y - dy[0] * direction.x, dx[0] * direction.x + dy[0] * direction.y);
1461
+ const wanted = -Math.max(-steering.limit, Math.min(steering.limit, error * steering.gain));
1462
+ this.steeringBias += (wanted - this.steeringBias) * Math.min(1, dt * steering.rate);
1463
+ return this.steeringBias;
1464
+ }
1465
+ _updateLinkTargets(throttle) {
1466
+ const channel = this.model.gait.gather;
1467
+ const phase = channel.harmonic * this.gait.phase;
1468
+ const phaseSine = Math.sin(phase);
1469
+ const phaseCosine = Math.cos(phase);
1470
+ const amplitude = channel.amplitude;
1471
+ const breathing = this.breathingScale;
1472
+ const targets = this.linkTargets;
1473
+ const { linkRestLength, gatherScale, gatherPhaseSine, gatherPhaseCosine, linkBreathes } = this.tables;
1474
+ for (let index = 0; index < targets.length; index++) {
1475
+ const wave = phaseCosine * gatherPhaseCosine[index] - phaseSine * gatherPhaseSine[index];
1476
+ const gather = 1 + amplitude * gatherScale[index] * throttle * wave;
1477
+ targets[index] = linkRestLength[index] * gather * (linkBreathes[index] ? 1 + breathing : 1);
1478
+ }
1479
+ }
1480
+ _relaxLinks() {
1481
+ const { x, y } = this.chain;
1482
+ const targets = this.linkTargets;
1483
+ const correctionHalf = this.tables.linkCorrectionHalf;
1484
+ const count = targets.length;
1485
+ let beforeX = x[0];
1486
+ let beforeY = y[0];
1487
+ for (let index = 0; index < count; index++) {
1488
+ const afterX = x[index + 1];
1489
+ const afterY = y[index + 1];
1490
+ const spanX = afterX - beforeX;
1491
+ const spanY = afterY - beforeY;
1492
+ const distance = Math.sqrt(spanX * spanX + spanY * spanY) || .001;
1493
+ const shift = (distance - targets[index]) / distance * correctionHalf[index];
1494
+ x[index] = beforeX + spanX * shift;
1495
+ y[index] = beforeY + spanY * shift;
1496
+ beforeX = afterX - spanX * shift;
1497
+ beforeY = afterY - spanY * shift;
1498
+ }
1499
+ x[count] = beforeX;
1500
+ y[count] = beforeY;
1501
+ }
1502
+ _clampLinks() {
1503
+ const { x, y } = this.chain;
1504
+ const targets = this.linkTargets;
1505
+ const count = targets.length;
1506
+ let beforeX = x[0];
1507
+ let beforeY = y[0];
1508
+ for (let index = 0; index < count; index++) {
1509
+ const limit = targets[index] * 3;
1510
+ const spanX = x[index + 1] - beforeX;
1511
+ const spanY = y[index + 1] - beforeY;
1512
+ const distance = Math.sqrt(spanX * spanX + spanY * spanY);
1513
+ if (distance > limit) {
1514
+ const scale = limit / distance;
1515
+ beforeX = beforeX + spanX * scale;
1516
+ beforeY = beforeY + spanY * scale;
1517
+ x[index + 1] = beforeX;
1518
+ y[index + 1] = beforeY;
1519
+ } else {
1520
+ beforeX = x[index + 1];
1521
+ beforeY = y[index + 1];
1522
+ }
1523
+ }
1524
+ }
1525
+ _applyAutoLift(dt, throttle) {
1526
+ const autoLift = this.model.physics.autoLift;
1527
+ if (!autoLift.amount) return;
1528
+ const { idle, gain, gaitContact, contact, count } = this.chain;
1529
+ const lifted = Math.round(autoLift.share * count);
1530
+ selectLowest(this.liftOrder, gain, lifted);
1531
+ this.liftTargets.fill(0);
1532
+ for (let index = 0; index < lifted; index++) this.liftTargets[this.liftOrder[index]] = throttle;
1533
+ const amount = Math.min(1, dt * autoLift.rate);
1534
+ for (let index = 0; index < count; index++) {
1535
+ idle[index] += (this.liftTargets[index] - idle[index]) * amount;
1536
+ contact[index] = Math.max(0, Math.min(1, gaitContact[index] * (1 - autoLift.amount * idle[index] * throttle)));
1537
+ }
1538
+ }
1539
+ getPose(pose) {
1540
+ const { x, y, dx, dy, count } = this.chain;
1541
+ const headX = x[0] - x[1];
1542
+ const headY = y[0] - y[1];
1543
+ const distance = magnitude$2(headX, headY);
1544
+ if (distance < 1e-9) {
1545
+ pose.direction.x = dx[0];
1546
+ pose.direction.y = dy[0];
1547
+ } else {
1548
+ pose.direction.x = headX / distance;
1549
+ pose.direction.y = headY / distance;
1550
+ }
1551
+ let centerX = 0;
1552
+ let centerY = 0;
1553
+ for (let index = 0; index < count; index++) {
1554
+ centerX += x[index] / count;
1555
+ centerY += y[index] / count;
1556
+ }
1557
+ pose.head.x = x[0];
1558
+ pose.head.y = y[0];
1559
+ pose.center.x = centerX;
1560
+ pose.center.y = centerY;
1561
+ return pose;
1562
+ }
1563
+ };
1564
+
1565
+ // src/legs.mjs
1566
+ /** Schema-v1 planted-foot state. Private to one Beefwife instance. */
1567
+ var lerp = (start, end, amount) => start + (end - start) * amount;
1568
+ var clamp = (value, low, high) => Math.max(low, Math.min(high, value));
1569
+ var magnitude$1 = /* @__PURE__ */ __name((x, y) => Math.sqrt(x * x + y * y), "magnitude");
1570
+ var limbLength = (reach, scale, fold) => {
1571
+ const amount = fold <= .5 ? lerp(.9, 1.35, fold * 2) : lerp(1.35, 1.72, (fold - .5) * 2);
1572
+ return reach * scale * amount;
1573
+ };
1574
+ var Legs = class {
1575
+ constructor(model, body, gait, random) {
1576
+ this.model = model;
1577
+ this.body = body;
1578
+ this.gait = gait;
1579
+ this.random = random;
1580
+ this.legs = [];
1581
+ this._build();
1582
+ }
1583
+ reconfigure(model, body, gait) {
1584
+ this.model = model;
1585
+ this.body = body;
1586
+ this.gait = gait;
1587
+ const anchors = this._anchors();
1588
+ this.legs.forEach((leg) => leg.anchor = anchors[leg.pair]);
1589
+ }
1590
+ _signedRandom() {
1591
+ return this.random() * 2 - 1;
1592
+ }
1593
+ _vary(amount) {
1594
+ return 1 + amount * this._signedRandom();
1595
+ }
1596
+ _anchors() {
1597
+ const { pairs, start, end } = this.model.legs;
1598
+ if (!pairs) return [];
1599
+ const firstDistance = this.model.chunks[start].restDistance;
1600
+ const lastDistance = this.model.chunks[end - 1].restDistance;
1601
+ return Array.from({ length: pairs }, (_, pair) => {
1602
+ const amount = pairs === 1 ? .5 : pair / (pairs - 1);
1603
+ const target = lerp(firstDistance, lastDistance, amount);
1604
+ let nearest = start;
1605
+ for (let index = start + 1; index < end; index++) if (Math.abs(this.model.chunks[index].restDistance - target) < Math.abs(this.model.chunks[nearest].restDistance - target)) nearest = index;
1606
+ return nearest;
1607
+ });
1608
+ }
1609
+ _build() {
1610
+ const options = this.model.legs;
1611
+ this._anchors().forEach((anchor, pair) => {
1612
+ [["left", -1], ["right", 1]].forEach(([side, sideSign]) => {
1613
+ const leg = {
1614
+ pair,
1615
+ anchor,
1616
+ side,
1617
+ sideSign,
1618
+ character: this._signedRandom(),
1619
+ reachScale: this._vary(options.jitter * .28),
1620
+ spreadScale: this._vary(options.jitter * .3),
1621
+ leadScale: this._vary(options.jitter * .45),
1622
+ dragScale: 1,
1623
+ swingScale: 1,
1624
+ liftAt: options.liftThreshold,
1625
+ scatter: {
1626
+ x: 0,
1627
+ y: 0
1628
+ },
1629
+ plantSpan: 0,
1630
+ progress: 1,
1631
+ contactLow: false,
1632
+ foot: {
1633
+ x: 0,
1634
+ y: 0
1635
+ },
1636
+ hold: {
1637
+ x: 0,
1638
+ y: 0
1639
+ },
1640
+ from: {
1641
+ x: 0,
1642
+ y: 0
1643
+ }
1644
+ };
1645
+ this._rollStep(leg);
1646
+ const planted = this._plantAt(leg);
1647
+ Object.assign(leg.foot, planted);
1648
+ Object.assign(leg.hold, planted);
1649
+ Object.assign(leg.from, planted);
1650
+ leg.plantSpan = magnitude$1(planted.x - this.body.chain.x[anchor], planted.y - this.body.chain.y[anchor]);
1651
+ this.legs.push(leg);
1652
+ });
1653
+ });
1654
+ }
1655
+ _rollStep(leg) {
1656
+ const jitter = this.model.legs.jitter;
1657
+ leg.dragScale = this._vary(jitter * .4) * (1 + jitter * .35 * leg.character);
1658
+ leg.swingScale = this._vary(jitter * .3);
1659
+ leg.liftAt = clamp(this.model.legs.liftThreshold + jitter * .2 * leg.character, 0, 1);
1660
+ const scatterRadius = Math.max(this.model.legs.reach, this.model.legs.spread) * jitter * .1;
1661
+ leg.scatter = {
1662
+ x: scatterRadius * this._signedRandom(),
1663
+ y: scatterRadius * this._signedRandom()
1664
+ };
1665
+ }
1666
+ _plantAt(leg) {
1667
+ const options = this.model.legs;
1668
+ const { x, y, dx, dy } = this.body.chain;
1669
+ const anchor = leg.anchor;
1670
+ const ahead = options.reach * leg.reachScale * (.5 + options.lead * .5) * leg.leadScale;
1671
+ const outward = options.spread * leg.spreadScale * leg.sideSign;
1672
+ return {
1673
+ x: x[anchor] + dx[anchor] * ahead - dy[anchor] * outward,
1674
+ y: y[anchor] + dy[anchor] * ahead + dx[anchor] * outward
1675
+ };
1676
+ }
1677
+ contactFor(leg, throttle) {
1678
+ const chunk = this.model.chunks[leg.anchor];
1679
+ const phaseOffset = leg.side === "right" ? Math.PI * this.model.legs.sidePhase : 0;
1680
+ return clamp(this.gait.contactAt(chunk.restDistance, throttle, chunk.motionScale.contact, phaseOffset) * (1 - this.model.physics.autoLift.amount * this.body.chain.idle[leg.anchor] * throttle), 0, 1);
1681
+ }
1682
+ update(dt, throttle) {
1683
+ const options = this.model.legs;
1684
+ const chain = this.body.chain;
1685
+ for (let index = 0; index < this.legs.length; index++) {
1686
+ const leg = this.legs[index];
1687
+ const hipX = chain.x[leg.anchor];
1688
+ const hipY = chain.y[leg.anchor];
1689
+ const hipDx = chain.dx[leg.anchor];
1690
+ const hipDy = chain.dy[leg.anchor];
1691
+ const low = this.contactFor(leg, throttle) < leg.liftAt;
1692
+ const released = low && !leg.contactLow;
1693
+ leg.contactLow = low;
1694
+ if (leg.progress < 1) {
1695
+ const seconds = options.swingSeconds * leg.swingScale;
1696
+ leg.progress = Math.min(1, leg.progress + dt / seconds);
1697
+ const arc = Math.sin(Math.PI * leg.progress) * options.swingArc;
1698
+ leg.foot.x = lerp(leg.from.x, leg.hold.x, leg.progress) - hipDy * arc * leg.sideSign;
1699
+ leg.foot.y = lerp(leg.from.y, leg.hold.y, leg.progress) + hipDx * arc * leg.sideSign;
1700
+ continue;
1701
+ }
1702
+ Object.assign(leg.foot, leg.hold);
1703
+ const footX = leg.foot.x - hipX;
1704
+ const footY = leg.foot.y - hipY;
1705
+ const forward = footX * hipDx + footY * hipDy;
1706
+ const reach = options.reach * leg.reachScale;
1707
+ const trailed = forward < -(reach * (.5 - options.lead * .5)) * leg.dragScale;
1708
+ const nominalSpan = magnitude$1(reach, options.spread * leg.spreadScale);
1709
+ const overextended = magnitude$1(footX, footY) > Math.max(nominalSpan, leg.plantSpan) * 1.55;
1710
+ if (released || trailed || overextended) {
1711
+ Object.assign(leg.from, leg.foot);
1712
+ const planted = this._plantAt(leg);
1713
+ leg.hold.x = planted.x + leg.scatter.x;
1714
+ leg.hold.y = planted.y + leg.scatter.y;
1715
+ leg.plantSpan = magnitude$1(leg.hold.x - hipX, leg.hold.y - hipY);
1716
+ leg.progress = 0;
1717
+ this._rollStep(leg);
1718
+ leg.contactLow = this.contactFor(leg, throttle) < leg.liftAt;
1719
+ }
1720
+ }
1721
+ }
1722
+ translate(offset) {
1723
+ this.legs.forEach((leg) => {
1724
+ [
1725
+ leg.foot,
1726
+ leg.hold,
1727
+ leg.from
1728
+ ].forEach((point) => {
1729
+ point.x += offset.x;
1730
+ point.y += offset.y;
1731
+ });
1732
+ });
1733
+ }
1734
+ armLength(leg) {
1735
+ return limbLength(this.model.legs.reach, leg.reachScale, this.model.legs.fold);
1736
+ }
1737
+ };
1738
+
1739
+ // src/skin.mjs
1740
+ /** Schema-v1 ornament motion and renderer snapshots. */
1741
+ var RENDER_LAYOUT = Object.freeze({
1742
+ chunkStride: 4,
1743
+ legStride: 11,
1744
+ ornamentStride: 6,
1745
+ plateStride: 5
1746
+ });
1747
+ var RADIAN_TURN_RATE = 13;
1748
+ var MAX_DEFLECTION = Math.PI / 2;
1749
+ var magnitude = (x, y) => Math.sqrt(x * x + y * y);
1750
+ var springCoefficients = (ornament, rate, zeta, dt) => {
1751
+ const angularDt = rate * dt;
1752
+ if (angularDt < 1e-9) {
1753
+ ornament.positionPosition = 1;
1754
+ ornament.positionVelocity = dt;
1755
+ ornament.velocityPosition = 0;
1756
+ ornament.velocityVelocity = 1;
1757
+ return;
1758
+ }
1759
+ const decay = Math.exp(-zeta * angularDt);
1760
+ if (zeta >= 1 - 1e-8) {
1761
+ ornament.positionPosition = decay * (1 + angularDt);
1762
+ ornament.positionVelocity = decay * dt;
1763
+ ornament.velocityPosition = -decay * rate * angularDt;
1764
+ ornament.velocityVelocity = decay * (1 - angularDt);
1765
+ return;
1766
+ }
1767
+ const ringRate = rate * Math.sqrt(1 - zeta * zeta);
1768
+ const cosine = Math.cos(ringRate * dt);
1769
+ const sine = Math.sin(ringRate * dt);
1770
+ const lean = zeta * rate / ringRate;
1771
+ ornament.positionPosition = decay * (cosine + lean * sine);
1772
+ ornament.positionVelocity = decay * sine / ringRate;
1773
+ ornament.velocityPosition = -decay * rate * rate * sine / ringRate;
1774
+ ornament.velocityVelocity = decay * (cosine - lean * sine);
1775
+ };
1776
+ var rootFor = (ornament, chain, root) => {
1777
+ const at = ornament.chunk;
1778
+ const dx = chain.dx[at];
1779
+ const dy = chain.dy[at];
1780
+ root.x = chain.x[at] + dx * ornament.offset.forward - dy * ornament.offset.outward * ornament.sideSign;
1781
+ root.y = chain.y[at] + dy * ornament.offset.forward + dx * ornament.offset.outward * ornament.sideSign;
1782
+ root.dx = dx * ornament.angleCosine - dy * ornament.angleSine;
1783
+ root.dy = dy * ornament.angleCosine + dx * ornament.angleSine;
1784
+ return root;
1785
+ };
1786
+ var jointFor = (hip, foot, arm, bow, joint) => {
1787
+ const x = foot.x - hip.x;
1788
+ const y = foot.y - hip.y;
1789
+ const distance = magnitude(x, y);
1790
+ const normalX = distance > .001 ? -y / distance : -hip.dy;
1791
+ const normalY = distance > .001 ? x / distance : hip.dx;
1792
+ const halfBone = arm / 2;
1793
+ const halfSpan = Math.min(distance, arm) / 2;
1794
+ const bend = Math.sqrt(Math.max(0, halfBone ** 2 - halfSpan ** 2));
1795
+ joint.x = hip.x + x / 2 + normalX * bend * bow;
1796
+ joint.y = hip.y + y / 2 + normalY * bend * bow;
1797
+ return joint;
1798
+ };
1799
+ var Skin = class {
1800
+ constructor(model, body, legs) {
1801
+ this.model = model;
1802
+ this.body = body;
1803
+ this.legs = legs;
1804
+ this.joint = {
1805
+ x: 0,
1806
+ y: 0
1807
+ };
1808
+ this.hip = {
1809
+ x: 0,
1810
+ y: 0,
1811
+ dx: 0,
1812
+ dy: 0
1813
+ };
1814
+ this.ornaments = [];
1815
+ this._buildOrnaments();
1816
+ }
1817
+ reconfigure(model, body, legs) {
1818
+ this.model = model;
1819
+ this.body = body;
1820
+ this.legs = legs;
1821
+ this.ornaments.forEach((ornament, index) => {
1822
+ ornament.spec = model.skin.ornaments[index];
1823
+ ornament.coefficientDt = null;
1824
+ const root = rootFor(ornament.spec, body.chain, ornament.root);
1825
+ const cosine = Math.cos(ornament.angle);
1826
+ const sine = Math.sin(ornament.angle);
1827
+ ornament.directionX = root.dx * cosine - root.dy * sine;
1828
+ ornament.directionY = root.dy * cosine + root.dx * sine;
1829
+ });
1830
+ }
1831
+ _buildOrnaments() {
1832
+ this.ornaments = this.model.skin.ornaments.map((spec) => {
1833
+ const root = rootFor(spec, this.body.chain, {});
1834
+ return {
1835
+ spec,
1836
+ root,
1837
+ nextRoot: { ...root },
1838
+ angle: 0,
1839
+ velocity: 0,
1840
+ directionX: root.dx,
1841
+ directionY: root.dy,
1842
+ coefficientDt: null,
1843
+ positionPosition: 1,
1844
+ positionVelocity: 0,
1845
+ velocityPosition: 0,
1846
+ velocityVelocity: 1
1847
+ };
1848
+ });
1849
+ }
1850
+ _fitRenderState(state) {
1851
+ if (!state) state = { layout: RENDER_LAYOUT };
1852
+ state.model = this.model;
1853
+ const lengths = {
1854
+ chunks: this.body.chain.count * RENDER_LAYOUT.chunkStride,
1855
+ legs: this.legs.legs.length * RENDER_LAYOUT.legStride,
1856
+ ornaments: this.ornaments.length * RENDER_LAYOUT.ornamentStride,
1857
+ plates: this.model.skin.platesTailFirst.length * RENDER_LAYOUT.plateStride
1858
+ };
1859
+ for (const [band, length] of Object.entries(lengths)) if (!state[band] || state[band].length !== length) state[band] = new Float64Array(length);
1860
+ return state;
1861
+ }
1862
+ writeRenderState(state) {
1863
+ state = this._fitRenderState(state);
1864
+ const chain = this.body.chain;
1865
+ const chunkStride = RENDER_LAYOUT.chunkStride;
1866
+ for (let index = 0; index < chain.count; index++) {
1867
+ const offset = index * chunkStride;
1868
+ state.chunks[offset] = chain.x[index];
1869
+ state.chunks[offset + 1] = chain.y[index];
1870
+ state.chunks[offset + 2] = chain.dx[index];
1871
+ state.chunks[offset + 3] = chain.dy[index];
1872
+ }
1873
+ const foot = this.model.legs.skin.foot;
1874
+ const legStride = RENDER_LAYOUT.legStride;
1875
+ const sectionStart = this.legs.legs.length ? this.model.chunks[this.model.legs.start].restDistance : 0;
1876
+ const sectionSpan = this.legs.legs.length ? this.model.chunks[this.model.legs.end - 1].restDistance - sectionStart : 0;
1877
+ const { jointBend, jointLeanCenter } = this.model.legs;
1878
+ for (let index = 0; index < this.legs.legs.length; index++) {
1879
+ const leg = this.legs.legs[index];
1880
+ const hip = this.hip;
1881
+ hip.x = chain.x[leg.anchor];
1882
+ hip.y = chain.y[leg.anchor];
1883
+ hip.dx = chain.dx[leg.anchor];
1884
+ hip.dy = chain.dy[leg.anchor];
1885
+ const arm = this.legs.armLength(leg);
1886
+ const joint = jointFor(hip, leg.foot, arm, leg.sideSign * jointBend, this.joint);
1887
+ const chainPosition = sectionSpan ? 2 * (this.model.chunks[leg.anchor].restDistance - sectionStart) / sectionSpan - 1 : 0;
1888
+ const offset = index * legStride;
1889
+ state.legs[offset] = hip.x;
1890
+ state.legs[offset + 1] = hip.y;
1891
+ state.legs[offset + 2] = joint.x;
1892
+ state.legs[offset + 3] = joint.y;
1893
+ state.legs[offset + 4] = leg.foot.x;
1894
+ state.legs[offset + 5] = leg.foot.y;
1895
+ state.legs[offset + 6] = hip.dx;
1896
+ state.legs[offset + 7] = hip.dy;
1897
+ state.legs[offset + 8] = foot.scale * (leg.progress < 1 ? 1 : foot.plantedScale);
1898
+ state.legs[offset + 9] = leg.sideSign;
1899
+ state.legs[offset + 10] = (chainPosition - jointLeanCenter) * arm;
1900
+ }
1901
+ const ornamentStride = RENDER_LAYOUT.ornamentStride;
1902
+ for (let index = 0; index < this.ornaments.length; index++) {
1903
+ const ornament = this.ornaments[index];
1904
+ const offset = index * ornamentStride;
1905
+ state.ornaments[offset] = ornament.root.x;
1906
+ state.ornaments[offset + 1] = ornament.root.y;
1907
+ state.ornaments[offset + 2] = ornament.directionX;
1908
+ state.ornaments[offset + 3] = ornament.directionY;
1909
+ state.ornaments[offset + 4] = ornament.spec.scale;
1910
+ state.ornaments[offset + 5] = ornament.spec.sideSign;
1911
+ }
1912
+ const plates = this.model.skin.platesTailFirst;
1913
+ const plateStride = RENDER_LAYOUT.plateStride;
1914
+ for (let index = 0; index < plates.length; index++) {
1915
+ const plate = plates[index];
1916
+ const at = plate.chunk;
1917
+ const offset = index * plateStride;
1918
+ state.plates[offset] = chain.x[at];
1919
+ state.plates[offset + 1] = chain.y[at];
1920
+ state.plates[offset + 2] = chain.dx[at];
1921
+ state.plates[offset + 3] = chain.dy[at];
1922
+ state.plates[offset + 4] = plate.scale * this.model.chunks[at].plateScale * (1 + this.model.skin.loadScale * chain.contact[at]);
1923
+ }
1924
+ return state;
1925
+ }
1926
+ update(dt) {
1927
+ for (let index = 0; index < this.ornaments.length; index++) {
1928
+ const ornament = this.ornaments[index];
1929
+ const { spec } = ornament;
1930
+ const previousRoot = ornament.root;
1931
+ const root = rootFor(spec, this.body.chain, ornament.nextRoot);
1932
+ if (ornament.coefficientDt !== dt) {
1933
+ ornament.coefficientDt = dt;
1934
+ springCoefficients(ornament, spec.recover, 1 - spec.wobble * .98, dt);
1935
+ }
1936
+ const turnRate = Math.atan2(previousRoot.dx * root.dy - previousRoot.dy * root.dx, previousRoot.dx * root.dx + previousRoot.dy * root.dy) / dt;
1937
+ const lateralRate = ((root.x - previousRoot.x) * -root.dy + (root.y - previousRoot.y) * root.dx) / dt;
1938
+ const target = Math.min(MAX_DEFLECTION, Math.max(-MAX_DEFLECTION, spec.waveGain * -turnRate / RADIAN_TURN_RATE + spec.physGain * -lateralRate / this.model.skin.lateralRate));
1939
+ const deviation = ornament.angle - target;
1940
+ let angle = target + deviation * ornament.positionPosition + ornament.velocity * ornament.positionVelocity;
1941
+ let velocity = deviation * ornament.velocityPosition + ornament.velocity * ornament.velocityVelocity;
1942
+ if (angle > MAX_DEFLECTION) {
1943
+ angle = MAX_DEFLECTION;
1944
+ velocity = 0;
1945
+ } else if (angle < -MAX_DEFLECTION) {
1946
+ angle = -MAX_DEFLECTION;
1947
+ velocity = 0;
1948
+ }
1949
+ ornament.angle = angle;
1950
+ ornament.velocity = velocity;
1951
+ const cosine = Math.cos(angle);
1952
+ const sine = Math.sin(angle);
1953
+ ornament.directionX = root.dx * cosine - root.dy * sine;
1954
+ ornament.directionY = root.dy * cosine + root.dx * sine;
1955
+ ornament.root = root;
1956
+ ornament.nextRoot = previousRoot;
1957
+ }
1958
+ }
1959
+ translate(offset) {
1960
+ this.ornaments.forEach((ornament) => {
1961
+ ornament.root.x += offset.x;
1962
+ ornament.root.y += offset.y;
1963
+ });
1964
+ }
1965
+ };
1966
+
1967
+ // src/geometry.mjs
1968
+ /** Vertex math and pixel snapping for a Beefwife's meshes and outlines. */
1969
+ var KNEE_SEGMENTS = 4;
1970
+ var KNEE_POINTS = 5;
1971
+ var LIMB_VERTICES = 14;
1972
+ var LIMB_FLOATS = 28;
1973
+ var limbIndicesFor = (legCount) => {
1974
+ const quads = 6;
1975
+ const indices = new Uint32Array(legCount * quads * 6);
1976
+ let at = 0;
1977
+ for (let leg = 0; leg < legCount; leg++) {
1978
+ const first = leg * LIMB_VERTICES;
1979
+ const last = first + LIMB_VERTICES - 1;
1980
+ for (let step = 0; step < quads; step++) {
1981
+ const down = first + step;
1982
+ const back = last - step;
1983
+ indices.set([
1984
+ down,
1985
+ back,
1986
+ down + 1,
1987
+ down + 1,
1988
+ back,
1989
+ back - 1
1990
+ ], at);
1991
+ at += 6;
1992
+ }
1993
+ }
1994
+ return indices;
1995
+ };
1996
+ var CAP_SEGMENTS = 12;
1997
+ var CAP_VERTICES = 14;
1998
+ var CAP_COSINE = /* @__PURE__ */ new Float64Array(13);
1999
+ var CAP_SINE = /* @__PURE__ */ new Float64Array(13);
2000
+ for (let step = 0; step <= 12; step++) {
2001
+ const angle = Math.PI * step / 12;
2002
+ CAP_COSINE[step] = Math.cos(angle);
2003
+ CAP_SINE[step] = Math.sin(angle);
2004
+ }
2005
+ var ribbonPositionsFor = (chunkCount) => new Float32Array((chunkCount * 2 + 28) * 2);
2006
+ var ribbonIndicesFor = (chunkCount) => {
2007
+ const quads = Math.max(0, chunkCount - 1);
2008
+ const indices = new Uint32Array(quads * 6 + 72);
2009
+ let at = 0;
2010
+ for (let chunk = 0; chunk < quads; chunk++) {
2011
+ const vertex = chunk * 2;
2012
+ indices.set([
2013
+ vertex,
2014
+ vertex + 1,
2015
+ vertex + 2,
2016
+ vertex + 2,
2017
+ vertex + 1,
2018
+ vertex + 3
2019
+ ], at);
2020
+ at += 6;
2021
+ }
2022
+ for (const hub of [chunkCount * 2, chunkCount * 2 + 14]) for (let step = 0; step < 12; step++) {
2023
+ indices.set([
2024
+ hub,
2025
+ hub + 1 + step,
2026
+ hub + 2 + step
2027
+ ], at);
2028
+ at += 3;
2029
+ }
2030
+ return indices;
2031
+ };
2032
+ var snapCoordinate = (value, pixelResolution, inversePixelResolution) => pixelResolution === 1 ? Math.round(value) : pixelResolution > 0 ? Math.round(value * pixelResolution) * inversePixelResolution : value;
2033
+ var snapPositions = (positions, start, end, pixelResolution, inversePixelResolution) => {
2034
+ if (pixelResolution === 1) for (let index = start; index < end; index++) positions[index] = Math.round(positions[index]);
2035
+ else if (pixelResolution > 0) for (let index = start; index < end; index++) positions[index] = Math.round(positions[index] * pixelResolution) * inversePixelResolution;
2036
+ };
2037
+ var writeCap = (positions, offset, x, y, radius, fromX, fromY, overX, overY, pixelResolution, inversePixelResolution) => {
2038
+ positions[offset] = x;
2039
+ positions[offset + 1] = y;
2040
+ for (let step = 0; step <= 12; step++) {
2041
+ const at = offset + 2 + step * 2;
2042
+ const cosine = CAP_COSINE[step];
2043
+ const sine = CAP_SINE[step];
2044
+ positions[at] = x + radius * (cosine * fromX + sine * overX);
2045
+ positions[at + 1] = y + radius * (cosine * fromY + sine * overY);
2046
+ }
2047
+ snapPositions(positions, offset, offset + 28, pixelResolution, inversePixelResolution);
2048
+ };
2049
+ var writeLimb = (positions, offset, hipX, hipY, kneeX, kneeY, footX, footY, width, pixelResolution = 0, inversePixelResolution = 0) => {
2050
+ const half = width * .5;
2051
+ const thighX = kneeX - hipX;
2052
+ const thighY = kneeY - hipY;
2053
+ const thighLength = Math.hypot(thighX, thighY) || 1;
2054
+ const shinX = footX - kneeX;
2055
+ const shinY = footY - kneeY;
2056
+ const shinLength = Math.hypot(shinX, shinY) || 1;
2057
+ const thighNormalX = -thighY / thighLength;
2058
+ const thighNormalY = thighX / thighLength;
2059
+ const shinNormalX = -shinY / shinLength;
2060
+ const shinNormalY = shinX / shinLength;
2061
+ const spread = 1 + thighNormalX * shinNormalX + thighNormalY * shinNormalY;
2062
+ let cornerX = thighNormalX;
2063
+ let cornerY = thighNormalY;
2064
+ if (spread > 1e-6) {
2065
+ cornerX = (thighNormalX + shinNormalX) / spread;
2066
+ cornerY = (thighNormalY + shinNormalY) / spread;
2067
+ const along = Math.abs((cornerX * thighX + cornerY * thighY) / thighLength * half);
2068
+ const bone = Math.min(thighLength, shinLength);
2069
+ if (along > bone) {
2070
+ cornerX = cornerX * bone / along;
2071
+ cornerY = cornerY * bone / along;
2072
+ }
2073
+ }
2074
+ const sweep = Math.atan2(thighNormalX * shinNormalY - thighNormalY * shinNormalX, thighNormalX * shinNormalX + thighNormalY * shinNormalY);
2075
+ const stepCosine = Math.cos(sweep / KNEE_SEGMENTS);
2076
+ const stepSine = Math.sin(sweep / KNEE_SEGMENTS);
2077
+ const arcLeads = sweep <= 0;
2078
+ let arcX = thighNormalX;
2079
+ let arcY = thighNormalY;
2080
+ const footAt = offset + 12;
2081
+ const heelAt = offset + 28 - 2;
2082
+ positions[offset] = hipX + thighNormalX * half;
2083
+ positions[offset + 1] = hipY + thighNormalY * half;
2084
+ positions[footAt] = footX + shinNormalX * half;
2085
+ positions[footAt + 1] = footY + shinNormalY * half;
2086
+ positions[footAt + 2] = footX - shinNormalX * half;
2087
+ positions[footAt + 3] = footY - shinNormalY * half;
2088
+ positions[heelAt] = hipX - thighNormalX * half;
2089
+ positions[heelAt + 1] = hipY - thighNormalY * half;
2090
+ for (let step = 0; step < 5; step++) {
2091
+ const leadX = arcLeads ? arcX : cornerX;
2092
+ const leadY = arcLeads ? arcY : cornerY;
2093
+ const trailX = arcLeads ? cornerX : arcX;
2094
+ const trailY = arcLeads ? cornerY : arcY;
2095
+ const leadAt = offset + (1 + step) * 2;
2096
+ const trailAt = offset + (12 - step) * 2;
2097
+ positions[leadAt] = kneeX + leadX * half;
2098
+ positions[leadAt + 1] = kneeY + leadY * half;
2099
+ positions[trailAt] = kneeX - trailX * half;
2100
+ positions[trailAt + 1] = kneeY - trailY * half;
2101
+ const turnedX = arcX * stepCosine - arcY * stepSine;
2102
+ arcY = arcX * stepSine + arcY * stepCosine;
2103
+ arcX = turnedX;
2104
+ }
2105
+ snapPositions(positions, offset, offset + 28, pixelResolution, inversePixelResolution);
2106
+ };
2107
+
2108
+ // src/display.mjs
2109
+ /** Pixi resource lifetime and construction for the parts a Beefwife draws. */
2110
+ var discard = (parent, child) => {
2111
+ if (child.parent === parent) parent.removeChild(child);
2112
+ const geometry = child.geometry ?? null;
2113
+ child.destroy();
2114
+ if (geometry) geometry.destroy(true);
2115
+ };
2116
+ var contextFor = (shape, paint, scale) => {
2117
+ const path = new PIXI.GraphicsPath(shape.path).transform(new PIXI.Matrix(scale, 0, 0, scale, 0, 0));
2118
+ const context = new PIXI.GraphicsContext().path(path);
2119
+ if (paint.fill !== null) context.fill(paint.fill);
2120
+ if (paint.stroke !== null && paint.strokeWidth > 0) context.stroke({
2121
+ color: paint.stroke,
2122
+ width: paint.strokeWidth * scale,
2123
+ cap: "butt",
2124
+ join: "miter"
2125
+ });
2126
+ return context;
2127
+ };
2128
+ var paintMesh = (mesh, fill) => {
2129
+ const color = new PIXI.Color(fill);
2130
+ mesh.tint = color.toNumber();
2131
+ mesh.alpha = color.alpha;
2132
+ };
2133
+ var meshFor = (positions, indices, color) => {
2134
+ const geometry = new PIXI.MeshGeometry({
2135
+ positions,
2136
+ uvs: new Float32Array(positions.length),
2137
+ indices,
2138
+ shrinkBuffersToFit: false
2139
+ });
2140
+ const mesh = new PIXI.Mesh({
2141
+ geometry,
2142
+ texture: PIXI.Texture.WHITE,
2143
+ roundPixels: false
2144
+ });
2145
+ paintMesh(mesh, color);
2146
+ mesh.dynamicPositions = positions;
2147
+ mesh.positionBuffer = geometry.getBuffer("aPosition");
2148
+ return mesh;
2149
+ };
2150
+
2151
+ // src/atlas.mjs
2152
+ /**
2153
+ * One texture holding every shape a Beefwife places, so its feet, plates and
2154
+ * ornaments draw as particles out of a shared frame instead of a Graphics
2155
+ * apiece. The work splits three ways: a plan names the frames from the
2156
+ * descriptor alone, packing measures and places them, and only the bake needs
2157
+ * a GPU. Naming is what a population repeats, so it is the step kept free of
2158
+ * the other two.
2159
+ */
2160
+ var BAKE_SUPERSAMPLE = 4;
2161
+ var PAD_STROKES = 1;
2162
+ var MIN_PAD_TEXELS = 1;
2163
+ var ATLAS_TEXEL_LIMIT = 2048;
2164
+ var FRAME_TEXEL_LIMIT = ATLAS_TEXEL_LIMIT / 2;
2165
+ var frameKeyFor = (shape, paint) => `${shape.path}|${paint.fill}|${paint.stroke}|${paint.strokeWidth}`;
2166
+ var baked = /* @__PURE__ */ new WeakMap();
2167
+ var prepared = /* @__PURE__ */ new WeakMap();
2168
+ var planAtlas = (model, renderResolution) => {
2169
+ const resolution = renderResolution * 4;
2170
+ const specs = /* @__PURE__ */ new Map();
2171
+ const claim = (shape, paint, scale) => {
2172
+ if (!(scale > 0)) return null;
2173
+ const key = frameKeyFor(shape, paint);
2174
+ const frame = specs.get(key);
2175
+ if (frame) frame.scale = Math.max(frame.scale, scale);
2176
+ else specs.set(key, {
2177
+ key,
2178
+ shape,
2179
+ paint,
2180
+ scale
2181
+ });
2182
+ return key;
2183
+ };
2184
+ const foot = model.legs.skin.foot;
2185
+ const feet = model.legs.pairs ? claim(foot.shape, foot.paint, foot.scale * Math.max(1, foot.plantedScale)) : null;
2186
+ const load = 1 + Math.max(0, model.skin.loadScale);
2187
+ const plates = model.skin.platesTailFirst.map((plate) => claim(plate.shape, plate.paint, plate.scale * model.chunks[plate.chunk].plateScale * load));
2188
+ const ornaments = model.skin.ornaments.map((ornament) => claim(ornament.shape, ornament.paint, ornament.scale));
2189
+ return {
2190
+ key: `${resolution}\n${[...specs.values()].map(({ key, scale }) => `${key}|${scale}`).sort().join("\n")}`,
2191
+ resolution,
2192
+ frames: [...specs.values()],
2193
+ feet,
2194
+ plates,
2195
+ ornaments
2196
+ };
2197
+ };
2198
+ var arrangeEntries = (entries) => {
2199
+ entries.sort((a, b) => b.height - a.height || a.key.localeCompare(b.key));
2200
+ let shelfX = 0;
2201
+ let shelfY = 0;
2202
+ let shelfHeight = 0;
2203
+ let width = 0;
2204
+ for (const entry of entries) {
2205
+ if (shelfX > 0 && shelfX + entry.width > 2048) {
2206
+ shelfX = 0;
2207
+ shelfY += shelfHeight;
2208
+ shelfHeight = 0;
2209
+ }
2210
+ entry.x = shelfX;
2211
+ entry.y = shelfY;
2212
+ shelfX += entry.width;
2213
+ shelfHeight = Math.max(shelfHeight, entry.height);
2214
+ width = Math.max(width, shelfX);
2215
+ }
2216
+ return {
2217
+ width,
2218
+ height: shelfY + shelfHeight
2219
+ };
2220
+ };
2221
+ /** Measure once, then fit frames by reducing their raster scale. */
2222
+ var packAtlas = (plan) => {
2223
+ const resolution = plan.resolution;
2224
+ const entries = [];
2225
+ try {
2226
+ for (const spec of plan.frames) {
2227
+ const context = contextFor(spec.shape, spec.paint, spec.scale);
2228
+ const entry = {
2229
+ ...spec,
2230
+ context,
2231
+ requestedScale: spec.scale
2232
+ };
2233
+ entries.push(entry);
2234
+ const bounds = context.bounds;
2235
+ entry.bounds = {
2236
+ minX: bounds.minX,
2237
+ minY: bounds.minY,
2238
+ width: bounds.width,
2239
+ height: bounds.height
2240
+ };
2241
+ if (!Object.values(entry.bounds).every(Number.isFinite)) throw new RangeError("atlas shape bounds must be finite");
2242
+ }
2243
+ let frameLimit = FRAME_TEXEL_LIMIT;
2244
+ let size;
2245
+ for (;;) {
2246
+ for (const entry of entries) {
2247
+ const { bounds, paint, requestedScale } = entry;
2248
+ const stroke = paint.strokeWidth * requestedScale * PAD_STROKES * resolution;
2249
+ const extent = Math.max(bounds.width, bounds.height) * resolution;
2250
+ const factor = Math.ceil(extent) + 2 * Math.max(MIN_PAD_TEXELS, Math.ceil(stroke)) <= frameLimit ? 1 : (frameLimit - 3) / (extent + 2 * stroke);
2251
+ entry.scale = requestedScale * factor;
2252
+ entry.pad = Math.max(MIN_PAD_TEXELS, Math.ceil(stroke * factor));
2253
+ entry.originX = entry.pad + Math.ceil(-bounds.minX * factor * resolution);
2254
+ entry.originY = entry.pad + Math.ceil(-bounds.minY * factor * resolution);
2255
+ entry.width = Math.ceil(bounds.width * factor * resolution) + entry.pad * 2;
2256
+ entry.height = Math.ceil(bounds.height * factor * resolution) + entry.pad * 2;
2257
+ }
2258
+ size = arrangeEntries(entries);
2259
+ if (size.width <= 2048 && size.height <= 2048) break;
2260
+ if (frameLimit === 4) throw new RangeError("too many atlas frames for the texture budget");
2261
+ frameLimit = Math.max(4, Math.floor(frameLimit * .75));
2262
+ }
2263
+ for (const entry of entries) {
2264
+ if (entry.scale === entry.requestedScale) continue;
2265
+ entry.context.destroy();
2266
+ entry.context = null;
2267
+ entry.context = contextFor(entry.shape, entry.paint, entry.scale);
2268
+ }
2269
+ return {
2270
+ resolution,
2271
+ ...size,
2272
+ entries
2273
+ };
2274
+ } catch (error) {
2275
+ for (const entry of entries) entry.context?.destroy();
2276
+ throw error;
2277
+ }
2278
+ };
2279
+ var prepareAtlas = (model, renderResolution) => {
2280
+ const held = prepared.get(model);
2281
+ if (held?.renderResolution === renderResolution) return held.plan;
2282
+ if (!Number.isFinite(renderResolution) || renderResolution <= 0) throw new RangeError("pixelResolution must be finite and positive");
2283
+ const plan = planAtlas(model, renderResolution);
2284
+ const sheet = packAtlas(plan);
2285
+ for (const entry of sheet.entries) entry.context.destroy();
2286
+ prepared.set(model, {
2287
+ renderResolution,
2288
+ plan
2289
+ });
2290
+ return plan;
2291
+ };
2292
+ var bakeAtlas = (plan, renderer) => {
2293
+ const sheet = packAtlas(plan);
2294
+ const texel = 1 / sheet.resolution;
2295
+ const target = PIXI.RenderTexture.create({
2296
+ width: sheet.width * texel,
2297
+ height: sheet.height * texel,
2298
+ resolution: sheet.resolution,
2299
+ antialias: false,
2300
+ scaleMode: "nearest"
2301
+ });
2302
+ const frames = /* @__PURE__ */ new Map();
2303
+ let complete = false;
2304
+ try {
2305
+ let clear = true;
2306
+ for (const entry of sheet.entries) {
2307
+ const graphics = new PIXI.Graphics(entry.context);
2308
+ try {
2309
+ renderer.render({
2310
+ container: graphics,
2311
+ target,
2312
+ clear,
2313
+ transform: new PIXI.Matrix(1, 0, 0, 1, (entry.x + entry.originX) * texel, (entry.y + entry.originY) * texel)
2314
+ });
2315
+ } finally {
2316
+ graphics.destroy();
2317
+ }
2318
+ clear = false;
2319
+ frames.set(entry.key, {
2320
+ texture: new PIXI.Texture({
2321
+ source: target.source,
2322
+ frame: new PIXI.Rectangle(entry.x * texel, entry.y * texel, entry.width * texel, entry.height * texel)
2323
+ }),
2324
+ scale: entry.scale,
2325
+ anchorX: entry.originX / entry.width,
2326
+ anchorY: entry.originY / entry.height
2327
+ });
2328
+ }
2329
+ complete = true;
2330
+ } finally {
2331
+ for (const entry of sheet.entries) entry.context.destroy();
2332
+ if (!complete) {
2333
+ for (const frame of frames.values()) frame.texture.destroy();
2334
+ target.destroy(true);
2335
+ }
2336
+ }
2337
+ return {
2338
+ key: plan.key,
2339
+ renderer,
2340
+ target,
2341
+ frames
2342
+ };
2343
+ };
2344
+ /**
2345
+ * Hands back the atlas a plan names, baking it the first time anyone asks.
2346
+ * Populations share one descriptor's frames, and a live edit that abandons a
2347
+ * set of frames takes the texture with it. Held per renderer, because a page
2348
+ * mounting two canvases gives each its own, and a texture belongs to the one
2349
+ * that made it.
2350
+ */
2351
+ var acquireAtlas = (plan, renderer) => {
2352
+ if (!plan.frames.length) return null;
2353
+ let sheets = baked.get(renderer);
2354
+ if (!sheets) baked.set(renderer, sheets = /* @__PURE__ */ new Map());
2355
+ let held = sheets.get(plan.key);
2356
+ if (!held) {
2357
+ held = {
2358
+ atlas: bakeAtlas(plan, renderer),
2359
+ uses: 0
2360
+ };
2361
+ sheets.set(plan.key, held);
2362
+ }
2363
+ held.uses++;
2364
+ return held.atlas;
2365
+ };
2366
+ var releaseAtlas = (atlas) => {
2367
+ const sheets = atlas && baked.get(atlas.renderer);
2368
+ const held = sheets && sheets.get(atlas.key);
2369
+ if (!held || --held.uses > 0) return;
2370
+ sheets.delete(atlas.key);
2371
+ for (const frame of atlas.frames.values()) frame.texture.destroy();
2372
+ atlas.target.source.removeAllListeners("change");
2373
+ atlas.target.destroy(true);
2374
+ };
2375
+
2376
+ // src/graphics.mjs
2377
+ /** Pixi display ownership for one Beefwife. */
2378
+ var BAND_LABELS = [
2379
+ "feet",
2380
+ "ornaments-under",
2381
+ "plates",
2382
+ "ornaments-over"
2383
+ ];
2384
+ var PARTICLE_PROPERTIES = {
2385
+ position: true,
2386
+ vertex: true,
2387
+ rotation: true,
2388
+ uvs: false,
2389
+ color: false
2390
+ };
2391
+ var HeadlessGraphics = class {
2392
+ static available = false;
2393
+ static prepare() {}
2394
+ static prepareAtlas() {}
2395
+ };
2396
+ var MAX_KNEE_OFFSET = MAX_WORLD_COORDINATE * 2;
2397
+ var pixelResolutionOf = (options) => {
2398
+ const value = options?.pixelResolution;
2399
+ if (value === void 0) return 1;
2400
+ if (!Number.isFinite(value)) throw new RangeError("pixelResolution must be finite and positive");
2401
+ return Math.max(MIN_PIXEL_RESOLUTION, Math.min(MAX_PIXEL_RESOLUTION, value));
2402
+ };
2403
+ /**
2404
+ * Runs work once the renderer is between frames.
2405
+ *
2406
+ * Pixi calls `onRender` partway through executing a frame's instructions, and
2407
+ * the renderer is one state machine: a nested `render` leaves the outer pass
2408
+ * holding a bind group the inner one replaced, and every display object still
2409
+ * queued draws from a texture with no source. Baking a sheet renders, and
2410
+ * destroying one frees what the pass is reading, so neither may happen there.
2411
+ * A microtask runs after the frame's callback returns and before the next, so
2412
+ * work booked here sees a renderer at rest.
2413
+ */
2414
+ var afterPass = (work) => queueMicrotask(work);
2415
+ var Graphics = class {
2416
+ static available = true;
2417
+ static prepare(model) {
2418
+ for (const [id, shape] of Object.entries(model.descriptor.definitions.shapes)) try {
2419
+ new PIXI.GraphicsPath(shape.path);
2420
+ } catch (error) {
2421
+ throw new Error(`$.definitions.shapes.${id}.path: ${error.message}`);
2422
+ }
2423
+ for (const [id, paint] of Object.entries(model.paints)) for (const key of ["fill", "stroke"]) {
2424
+ if (paint[key] === null) continue;
2425
+ try {
2426
+ new PIXI.Color(paint[key]);
2427
+ } catch (error) {
2428
+ throw new Error(`$.definitions.paints.${id}.${key === "stroke" ? "stroke.colour" : key}: ${error.message}`);
2429
+ }
2430
+ }
2431
+ }
2432
+ static prepareAtlas(model, options) {
2433
+ return prepareAtlas(model, pixelResolutionOf(options));
2434
+ }
2435
+ constructor(host, state, options = null) {
2436
+ this.host = host;
2437
+ this.failedBake = null;
2438
+ this.parent = host.addChild(new PIXI.Container());
2439
+ this.model = state.model;
2440
+ this.options = options || {};
2441
+ this.footParticles = [];
2442
+ this.ornamentParticles = [];
2443
+ this.plateParticles = [];
2444
+ this.shapeContainers = [];
2445
+ this.atlas = null;
2446
+ this.atlasRenderer = null;
2447
+ this.bakeRenderer = null;
2448
+ this.plan = null;
2449
+ this.legCount = 0;
2450
+ this.limbPositions = null;
2451
+ this.limbFill = null;
2452
+ this.limbStroke = null;
2453
+ this.limbCount = -1;
2454
+ this.ribbonPositions = null;
2455
+ this.ribbonFill = null;
2456
+ this.ribbonStroke = null;
2457
+ this.ribbonCount = -1;
2458
+ this.reportedResolution = null;
2459
+ this.baking = false;
2460
+ this.state = null;
2461
+ this.adopt(state);
2462
+ }
2463
+ adopt(state) {
2464
+ const resolution = pixelResolutionOf(this.options);
2465
+ const nextPlan = prepareAtlas(state.model, resolution);
2466
+ const legCount = state.legs.length / state.layout.legStride;
2467
+ if (this._samePlacements(nextPlan, state.model, legCount)) {
2468
+ if (this._sameFrames(nextPlan)) this.plan = nextPlan;
2469
+ } else {
2470
+ this.plan = null;
2471
+ this.footParticles = [];
2472
+ this.ornamentParticles = [];
2473
+ this.plateParticles = [];
2474
+ }
2475
+ this.nextPlan = nextPlan;
2476
+ this.nextResolution = resolution;
2477
+ this.failedBake = null;
2478
+ this.model = state.model;
2479
+ this.legCount = legCount;
2480
+ let changed = this._syncLimbParts(this.legCount);
2481
+ changed = this._syncRibbonParts() || changed;
2482
+ if (changed) this._arrange();
2483
+ this.sync(state);
2484
+ }
2485
+ _samePlacements(nextPlan, nextModel, legCount) {
2486
+ const plan = this.plan;
2487
+ if (!plan || legCount !== this.legCount) return false;
2488
+ const drawn = (a, b) => a === null === (b === null);
2489
+ return drawn(plan.feet, nextPlan.feet) && plan.plates.length === nextPlan.plates.length && plan.plates.every((key, index) => drawn(key, nextPlan.plates[index])) && plan.ornaments.length === nextPlan.ornaments.length && plan.ornaments.every((key, index) => drawn(key, nextPlan.ornaments[index]) && this.model.skin.ornaments[index].layer === nextModel.skin.ornaments[index].layer);
2490
+ }
2491
+ _sameFrames(nextPlan) {
2492
+ const plan = this.plan;
2493
+ return plan.key === nextPlan.key && plan.feet === nextPlan.feet && plan.plates.every((key, index) => key === nextPlan.plates[index]) && plan.ornaments.every((key, index) => key === nextPlan.ornaments[index]);
2494
+ }
2495
+ _drop(child) {
2496
+ discard(this.parent, child);
2497
+ }
2498
+ _syncLimbParts(legCount) {
2499
+ const paint = this.model.legs.skin.limbPaint;
2500
+ const wantFill = legCount > 0 && paint.fill !== null;
2501
+ const wantStroke = legCount > 0 && paint.stroke !== null && paint.strokeWidth > 0;
2502
+ const resized = legCount !== this.limbCount;
2503
+ let changed = false;
2504
+ if (resized) {
2505
+ this.limbCount = legCount;
2506
+ this.limbPositions = new Float32Array(legCount * 28);
2507
+ }
2508
+ if (this.limbFill && (resized || !wantFill)) {
2509
+ this._drop(this.limbFill);
2510
+ this.limbFill = null;
2511
+ changed = true;
2512
+ }
2513
+ if (wantFill && !this.limbFill) {
2514
+ this.limbFill = meshFor(this.limbPositions, limbIndicesFor(legCount), paint.fill);
2515
+ changed = true;
2516
+ } else if (this.limbFill) paintMesh(this.limbFill, paint.fill);
2517
+ if (this.limbStroke && !wantStroke) {
2518
+ this._drop(this.limbStroke);
2519
+ this.limbStroke = null;
2520
+ changed = true;
2521
+ } else if (wantStroke && !this.limbStroke) {
2522
+ this.limbStroke = new PIXI.Graphics();
2523
+ changed = true;
2524
+ }
2525
+ return changed;
2526
+ }
2527
+ _syncRibbonParts() {
2528
+ const paint = this.model.skin.ribbonPaint;
2529
+ const chunkCount = this.model.chunks.length;
2530
+ const wantFill = paint.fill !== null;
2531
+ const wantStroke = paint.stroke !== null && paint.strokeWidth > 0;
2532
+ const resized = chunkCount !== this.ribbonCount;
2533
+ let changed = false;
2534
+ if (resized) {
2535
+ this.ribbonCount = chunkCount;
2536
+ this.ribbonPositions = ribbonPositionsFor(chunkCount);
2537
+ }
2538
+ if (this.ribbonFill && (resized || !wantFill)) {
2539
+ this._drop(this.ribbonFill);
2540
+ this.ribbonFill = null;
2541
+ changed = true;
2542
+ }
2543
+ if (wantFill && !this.ribbonFill) {
2544
+ this.ribbonFill = meshFor(this.ribbonPositions, ribbonIndicesFor(chunkCount), paint.fill);
2545
+ changed = true;
2546
+ } else if (this.ribbonFill) paintMesh(this.ribbonFill, paint.fill);
2547
+ if (this.ribbonStroke && !wantStroke) {
2548
+ this._drop(this.ribbonStroke);
2549
+ this.ribbonStroke = null;
2550
+ changed = true;
2551
+ } else if (wantStroke && !this.ribbonStroke) {
2552
+ this.ribbonStroke = new PIXI.Graphics();
2553
+ changed = true;
2554
+ }
2555
+ return changed;
2556
+ }
2557
+ _arrange() {
2558
+ const [feet, under, plates, over] = this.shapeContainers;
2559
+ for (const child of [
2560
+ feet,
2561
+ this.limbFill,
2562
+ this.limbStroke,
2563
+ under,
2564
+ this.ribbonFill,
2565
+ this.ribbonStroke,
2566
+ plates,
2567
+ over
2568
+ ]) if (child) this.parent.addChild(child);
2569
+ }
2570
+ _buildParticles(plan) {
2571
+ const bands = [
2572
+ null,
2573
+ null,
2574
+ null,
2575
+ null
2576
+ ];
2577
+ const bandFor = (index) => {
2578
+ if (!bands[index]) bands[index] = new PIXI.ParticleContainer({
2579
+ label: BAND_LABELS[index],
2580
+ dynamicProperties: PARTICLE_PROPERTIES
2581
+ });
2582
+ return bands[index];
2583
+ };
2584
+ const place = (index, key) => {
2585
+ const frame = key === null ? null : this.atlas.frames.get(key);
2586
+ if (!frame) return null;
2587
+ const particle = new PIXI.Particle({
2588
+ texture: frame.texture,
2589
+ anchorX: frame.anchorX,
2590
+ anchorY: frame.anchorY
2591
+ });
2592
+ particle.bakeScale = frame.scale;
2593
+ bandFor(index).addParticle(particle);
2594
+ return particle;
2595
+ };
2596
+ this.footParticles = Array.from({ length: this.legCount }, () => place(0, plan.feet));
2597
+ this.ornamentParticles = plan.ornaments.map((key, index) => place(this.model.skin.ornaments[index].layer === "under" ? 1 : 3, key));
2598
+ this.plateParticles = plan.plates.map((key) => place(2, key));
2599
+ this.shapeContainers = bands;
2600
+ this._arrange();
2601
+ }
2602
+ _syncAtlas(renderer) {
2603
+ const resolution = this._liveResolution();
2604
+ if (resolution !== this.nextResolution) {
2605
+ this.nextPlan = prepareAtlas(this.model, resolution);
2606
+ this.nextResolution = resolution;
2607
+ }
2608
+ if (!renderer) return;
2609
+ if (this.plan === this.nextPlan && this.atlasRenderer === renderer) return;
2610
+ if (this.failedBake?.plan === this.nextPlan && this.failedBake.renderer === renderer) return;
2611
+ if (this.atlasRenderer && this.atlasRenderer !== renderer) {
2612
+ for (const container of this.shapeContainers) if (container) container.visible = false;
2613
+ }
2614
+ this.bakeRenderer = renderer;
2615
+ if (this.baking) return;
2616
+ this.baking = true;
2617
+ afterPass(() => {
2618
+ this.baking = false;
2619
+ if (this.parent.destroyed) return;
2620
+ try {
2621
+ this._bake(this.bakeRenderer);
2622
+ } catch (error) {
2623
+ this.failedBake = {
2624
+ plan: this.nextPlan,
2625
+ renderer: this.bakeRenderer
2626
+ };
2627
+ if (!this.host.emit("error", error)) throw error;
2628
+ }
2629
+ });
2630
+ }
2631
+ _liveResolution() {
2632
+ const value = this.options.pixelResolution;
2633
+ try {
2634
+ const resolution = pixelResolutionOf(this.options);
2635
+ this.reportedResolution = null;
2636
+ return resolution;
2637
+ } catch (error) {
2638
+ if (!(error instanceof RangeError)) throw error;
2639
+ const reported = this.reportedResolution;
2640
+ if (!reported || !Object.is(reported.value, value)) {
2641
+ this.reportedResolution = { value };
2642
+ afterPass(() => {
2643
+ if (this.parent.destroyed) return;
2644
+ if (!this.host.emit("error", error)) throw error;
2645
+ });
2646
+ }
2647
+ return this.nextResolution;
2648
+ }
2649
+ }
2650
+ _bake(renderer) {
2651
+ const plan = this.nextPlan;
2652
+ const atlas = acquireAtlas(plan, renderer);
2653
+ const spent = this.atlas;
2654
+ const replaced = this.shapeContainers;
2655
+ this.atlas = atlas;
2656
+ this.atlasRenderer = renderer;
2657
+ this.plan = plan;
2658
+ this._buildParticles(plan);
2659
+ const pixelResolution = this._snapResolution();
2660
+ this._placeParticles(this.state, pixelResolution, pixelResolution > 0 ? 1 / pixelResolution : 0);
2661
+ for (const container of replaced) if (container) container.destroy();
2662
+ releaseAtlas(spent);
2663
+ }
2664
+ _place(particle, x, y, directionX, directionY, scale, mirror, pixelResolution, inversePixelResolution) {
2665
+ if (!particle) return;
2666
+ particle.x = snapCoordinate(x, pixelResolution, inversePixelResolution);
2667
+ particle.y = snapCoordinate(y, pixelResolution, inversePixelResolution);
2668
+ particle.rotation = Math.atan2(directionY, directionX);
2669
+ const drawn = scale / particle.bakeScale;
2670
+ particle.scaleX = drawn;
2671
+ particle.scaleY = drawn * mirror;
2672
+ }
2673
+ _syncLimbs(state, pixelResolution, inversePixelResolution) {
2674
+ const legs = state.legs;
2675
+ const width = this.model.legs.skin.limbWidth;
2676
+ const positions = this.limbPositions;
2677
+ if (this.limbStroke) this.limbStroke.clear();
2678
+ if (width <= 0) {
2679
+ positions.fill(0);
2680
+ if (this.limbFill) this.limbFill.positionBuffer.update();
2681
+ return;
2682
+ }
2683
+ const stride = state.layout.legStride;
2684
+ const projection = this.options.kneeProjection ?? null;
2685
+ let centerX = projection?.centerX;
2686
+ let centerY = projection?.centerY;
2687
+ let perspective = projection?.perspective;
2688
+ if (Number.isFinite(centerX) && Number.isFinite(centerY) && Number.isFinite(perspective)) {
2689
+ centerX = Math.max(-MAX_WORLD_COORDINATE, Math.min(MAX_WORLD_COORDINATE, centerX));
2690
+ centerY = Math.max(-MAX_WORLD_COORDINATE, Math.min(MAX_WORLD_COORDINATE, centerY));
2691
+ perspective = Math.max(0, Math.min(MAX_PERSPECTIVE, perspective));
2692
+ } else perspective = 0;
2693
+ const requestedOffset = projection?.maxOffset;
2694
+ const maxOffset = Number.isFinite(requestedOffset) ? Math.max(0, Math.min(MAX_WORLD_COORDINATE, requestedOffset)) : MAX_KNEE_OFFSET;
2695
+ const jointLean = this.model.legs.jointLean;
2696
+ for (let offset = 0; offset < legs.length; offset += stride) {
2697
+ const vertexOffset = offset / stride * 28;
2698
+ let kneeX = legs[offset + 2];
2699
+ let kneeY = legs[offset + 3];
2700
+ if (perspective > 0) {
2701
+ const viewDistance = Math.hypot(kneeX - centerX, kneeY - centerY);
2702
+ if (viewDistance > 0) {
2703
+ const elbowHeight = Math.hypot(kneeX - (legs[offset] + legs[offset + 4]) / 2, kneeY - (legs[offset + 1] + legs[offset + 5]) / 2);
2704
+ const radialOffset = Math.min(maxOffset, viewDistance * elbowHeight * perspective);
2705
+ kneeX += (kneeX - centerX) / viewDistance * radialOffset;
2706
+ kneeY += (kneeY - centerY) / viewDistance * radialOffset;
2707
+ }
2708
+ }
2709
+ if (jointLean !== 0) {
2710
+ const leanOffset = legs[offset + 10] * jointLean;
2711
+ kneeX += legs[offset + 6] * leanOffset;
2712
+ kneeY += legs[offset + 7] * leanOffset;
2713
+ }
2714
+ writeLimb(positions, vertexOffset, legs[offset], legs[offset + 1], kneeX, kneeY, legs[offset + 4], legs[offset + 5], width, pixelResolution, inversePixelResolution);
2715
+ }
2716
+ if (this.limbFill) this.limbFill.positionBuffer.update();
2717
+ if (!this.limbStroke) return;
2718
+ for (let base = 0; base < positions.length; base += 28) {
2719
+ let lastX = positions[base];
2720
+ let lastY = positions[base + 1];
2721
+ this.limbStroke.moveTo(lastX, lastY);
2722
+ for (let vertex = 2; vertex < 28; vertex += 2) {
2723
+ const x = positions[base + vertex];
2724
+ const y = positions[base + vertex + 1];
2725
+ if (x === lastX && y === lastY) continue;
2726
+ this.limbStroke.lineTo(x, y);
2727
+ lastX = x;
2728
+ lastY = y;
2729
+ }
2730
+ this.limbStroke.closePath();
2731
+ }
2732
+ const legPaint = this.model.legs.skin.limbPaint;
2733
+ this.limbStroke.stroke({
2734
+ color: legPaint.stroke,
2735
+ width: legPaint.strokeWidth,
2736
+ cap: "butt",
2737
+ join: "bevel"
2738
+ });
2739
+ }
2740
+ _syncRibbon(state, pixelResolution, inversePixelResolution) {
2741
+ const chunks = state.chunks;
2742
+ const stride = state.layout.chunkStride;
2743
+ const count = this.model.chunks.length;
2744
+ const lastIndex = count - 1;
2745
+ const positions = this.ribbonPositions;
2746
+ for (let index = 0; index < count; index++) {
2747
+ const chunkOffset = index * stride;
2748
+ const vertexOffset = index * 4;
2749
+ const width = this.model.chunks[index].ribbonWidth;
2750
+ positions[vertexOffset] = chunks[chunkOffset] - chunks[chunkOffset + 3] * width;
2751
+ positions[vertexOffset + 1] = chunks[chunkOffset + 1] + chunks[chunkOffset + 2] * width;
2752
+ positions[vertexOffset + 2] = chunks[chunkOffset] + chunks[chunkOffset + 3] * width;
2753
+ positions[vertexOffset + 3] = chunks[chunkOffset + 1] - chunks[chunkOffset + 2] * width;
2754
+ snapPositions(positions, vertexOffset, vertexOffset + 4, pixelResolution, inversePixelResolution);
2755
+ }
2756
+ const tailOffset = lastIndex * stride;
2757
+ const tailX = chunks[tailOffset + 2];
2758
+ const tailY = chunks[tailOffset + 3];
2759
+ writeCap(positions, count * 4, chunks[tailOffset], chunks[tailOffset + 1], this.model.chunks[lastIndex].ribbonWidth, -tailY, tailX, -tailX, -tailY, pixelResolution, inversePixelResolution);
2760
+ const headX = chunks[2];
2761
+ const headY = chunks[3];
2762
+ writeCap(positions, count * 4 + 14 * 2, chunks[0], chunks[1], this.model.chunks[0].ribbonWidth, headY, -headX, headX, headY, pixelResolution, inversePixelResolution);
2763
+ if (this.ribbonFill) this.ribbonFill.positionBuffer.update();
2764
+ if (!this.ribbonStroke) return;
2765
+ this.ribbonStroke.clear();
2766
+ if (!this.model.skin.hasRibbon) return;
2767
+ const tailRim = count * 4 + 2;
2768
+ const headRim = count * 4 + 14 * 2 + 2;
2769
+ this.ribbonStroke.moveTo(positions[0], positions[1]);
2770
+ for (let index = 1; index < count; index++) this.ribbonStroke.lineTo(positions[index * 4], positions[index * 4 + 1]);
2771
+ for (let step = 1; step < 12; step++) this.ribbonStroke.lineTo(positions[tailRim + step * 2], positions[tailRim + step * 2 + 1]);
2772
+ for (let index = lastIndex; index >= 0; index--) this.ribbonStroke.lineTo(positions[index * 4 + 2], positions[index * 4 + 3]);
2773
+ for (let step = 1; step < 12; step++) this.ribbonStroke.lineTo(positions[headRim + step * 2], positions[headRim + step * 2 + 1]);
2774
+ this.ribbonStroke.closePath();
2775
+ const paint = this.model.skin.ribbonPaint;
2776
+ this.ribbonStroke.stroke({
2777
+ color: paint.stroke,
2778
+ width: paint.strokeWidth
2779
+ });
2780
+ }
2781
+ sync(state, renderer = null, drawable = true) {
2782
+ if (state.model !== this.model) throw new Error("render state model does not match Beefwife graphics");
2783
+ this.state = state;
2784
+ this._syncAtlas(renderer);
2785
+ if (!drawable) return;
2786
+ const pixelResolution = this._snapResolution();
2787
+ const inversePixelResolution = pixelResolution > 0 ? 1 / pixelResolution : 0;
2788
+ this._syncLimbs(state, pixelResolution, inversePixelResolution);
2789
+ this._syncRibbon(state, pixelResolution, inversePixelResolution);
2790
+ this._placeParticles(state, pixelResolution, inversePixelResolution);
2791
+ }
2792
+ _snapResolution() {
2793
+ return this.options.roundVertices === true ? this.nextResolution : 0;
2794
+ }
2795
+ _placeParticles(state, pixelResolution, inversePixelResolution) {
2796
+ const legs = state.legs;
2797
+ for (let index = 0; index < this.footParticles.length; index++) {
2798
+ const offset = index * state.layout.legStride;
2799
+ this._place(this.footParticles[index], legs[offset + 4], legs[offset + 5], legs[offset + 6], legs[offset + 7], legs[offset + 8], legs[offset + 9], pixelResolution, inversePixelResolution);
2800
+ }
2801
+ const ornaments = state.ornaments;
2802
+ for (let index = 0; index < this.ornamentParticles.length; index++) {
2803
+ const offset = index * state.layout.ornamentStride;
2804
+ this._place(this.ornamentParticles[index], ornaments[offset], ornaments[offset + 1], ornaments[offset + 2], ornaments[offset + 3], ornaments[offset + 4], ornaments[offset + 5], pixelResolution, inversePixelResolution);
2805
+ }
2806
+ const plates = state.plates;
2807
+ for (let index = 0; index < this.plateParticles.length; index++) {
2808
+ const offset = index * state.layout.plateStride;
2809
+ this._place(this.plateParticles[index], plates[offset], plates[offset + 1], plates[offset + 2], plates[offset + 3], plates[offset + 4], 1, pixelResolution, inversePixelResolution);
2810
+ }
2811
+ }
2812
+ destroy() {
2813
+ const children = [
2814
+ ...this.shapeContainers,
2815
+ this.limbFill,
2816
+ this.limbStroke,
2817
+ this.ribbonFill,
2818
+ this.ribbonStroke
2819
+ ];
2820
+ for (const child of children) if (child) discard(this.parent, child);
2821
+ releaseAtlas(this.atlas);
2822
+ this.atlas = null;
2823
+ this.parent.destroy();
2824
+ }
2825
+ };
2826
+ var graphics_default = available ? Graphics : HeadlessGraphics;
2827
+
2828
+ // src/beefwife.mjs
2829
+ /** Public lifecycle for one schema-v1 beefwife. */
2830
+ var HeadlessContainer = class {
2831
+ destroy() {
2832
+ this.destroyed = true;
2833
+ }
2834
+ };
2835
+ var Container = available ? PIXI.Container : HeadlessContainer;
2836
+ var compiled = /* @__PURE__ */ new WeakMap();
2837
+ var freezeDeep = (value) => {
2838
+ if (!value || typeof value !== "object") return value;
2839
+ Object.values(value).forEach(freezeDeep);
2840
+ return Object.freeze(value);
2841
+ };
2842
+ /**
2843
+ * One model per descriptor object, so a cast of a thousand compiles once.
2844
+ *
2845
+ * The cache is keyed on the object, which makes the object the promise: edit
2846
+ * one in place after handing it over and every creature drawn from it keeps
2847
+ * the model built before the edit. Freezing it turns that into a throw at the
2848
+ * line doing the editing. Pass a copy to change a descriptor.
2849
+ */
2850
+ var modelFor = (descriptor) => {
2851
+ const held = compiled.get(descriptor);
2852
+ if (held) return held;
2853
+ const model = compile(descriptor);
2854
+ graphics_default.prepare(model);
2855
+ compiled.set(descriptor, model);
2856
+ freezeDeep(descriptor);
2857
+ return model;
2858
+ };
2859
+ var TAU = Math.PI * 2;
2860
+ var OPTION_KEYS = /* @__PURE__ */ new Set([
2861
+ "position",
2862
+ "direction",
2863
+ "phase",
2864
+ "random",
2865
+ "render"
2866
+ ]);
2867
+ var RENDER_KEYS = /* @__PURE__ */ new Set([
2868
+ "roundVertices",
2869
+ "pixelResolution",
2870
+ "kneeProjection"
2871
+ ]);
2872
+ var KNEE_PROJECTION_KEYS = /* @__PURE__ */ new Set([
2873
+ "centerX",
2874
+ "centerY",
2875
+ "perspective",
2876
+ "maxOffset"
2877
+ ]);
2878
+ var RESET_KEYS = /* @__PURE__ */ new Set([
2879
+ "position",
2880
+ "direction",
2881
+ "phase"
2882
+ ]);
2883
+ var CONTROL_KEYS = /* @__PURE__ */ new Set(["throttle", "direction"]);
2884
+ var plainObject = (value, path) => {
2885
+ if (!value || typeof value !== "object" || Array.isArray(value)) throw new TypeError(`${path} must be an object`);
2886
+ const prototype = Object.getPrototypeOf(value);
2887
+ if (prototype !== Object.prototype && prototype !== null) throw new TypeError(`${path} must be a plain object`);
2888
+ return value;
2889
+ };
2890
+ var optionsOf = (value, allowed, path) => {
2891
+ if (value === void 0) return {};
2892
+ const options = plainObject(value, path);
2893
+ for (const key in options) if (!allowed.has(key)) throw new TypeError(`${path}.${key} is unknown`);
2894
+ return options;
2895
+ };
2896
+ var defaulted = (value, fallback) => value === void 0 ? fallback : value;
2897
+ var finite = (value, path) => {
2898
+ if (typeof value !== "number" || !Number.isFinite(value)) throw new TypeError(`${path} must be a finite number`);
2899
+ return value;
2900
+ };
2901
+ var renderOptionsOf = (value) => {
2902
+ if (value === void 0) return null;
2903
+ const render = optionsOf(value, RENDER_KEYS, "options.render");
2904
+ if (render.roundVertices !== void 0 && typeof render.roundVertices !== "boolean") throw new TypeError("options.render.roundVertices must be a boolean");
2905
+ if (render.pixelResolution !== void 0) {
2906
+ const pixelResolution = finite(render.pixelResolution, "options.render.pixelResolution");
2907
+ if (pixelResolution <= 0) throw new RangeError("options.render.pixelResolution must be positive");
2908
+ if (pixelResolution < 1e-6 || pixelResolution > 1e6) throw new RangeError(`options.render.pixelResolution must be from ${MIN_PIXEL_RESOLUTION} to ${MAX_PIXEL_RESOLUTION}`);
2909
+ }
2910
+ const projection = render.kneeProjection;
2911
+ if (projection !== void 0 && projection !== null) {
2912
+ optionsOf(projection, KNEE_PROJECTION_KEYS, "options.render.kneeProjection");
2913
+ worldPoint({
2914
+ x: projection.centerX,
2915
+ y: projection.centerY
2916
+ }, null, "options.render.kneeProjection.center");
2917
+ const perspective = finite(projection.perspective, "options.render.kneeProjection.perspective");
2918
+ if (perspective < 0) throw new RangeError("options.render.kneeProjection.perspective must be nonnegative");
2919
+ if (perspective > 1e6) throw new RangeError(`options.render.kneeProjection.perspective must be at most ${MAX_PERSPECTIVE}`);
2920
+ if (projection.maxOffset !== void 0) {
2921
+ const maxOffset = finite(projection.maxOffset, "options.render.kneeProjection.maxOffset");
2922
+ if (maxOffset < 0) throw new RangeError("options.render.kneeProjection.maxOffset must be nonnegative");
2923
+ if (maxOffset > 1e9) throw new RangeError(`options.render.kneeProjection.maxOffset must be at most ${MAX_WORLD_COORDINATE}`);
2924
+ }
2925
+ }
2926
+ return render;
2927
+ };
2928
+ var point = (value, fallback, path) => {
2929
+ if (value === void 0) {
2930
+ if (fallback === null) throw new TypeError(`${path} is required`);
2931
+ return { ...fallback };
2932
+ }
2933
+ const input = plainObject(value, path);
2934
+ for (const key in input) if (key !== "x" && key !== "y") throw new TypeError(`${path}.${key} is unknown`);
2935
+ return {
2936
+ x: finite(input.x, `${path}.x`),
2937
+ y: finite(input.y, `${path}.y`)
2938
+ };
2939
+ };
2940
+ var worldPoint = (value, fallback, path) => {
2941
+ const result = point(value, fallback, path);
2942
+ if (Math.abs(result.x) > 1e9 || Math.abs(result.y) > 1e9) throw new RangeError(`${path} coordinates must be from ${-MAX_WORLD_COORDINATE} to ${MAX_WORLD_COORDINATE}`);
2943
+ return result;
2944
+ };
2945
+ var directionInto = (value, fallback, path, result) => {
2946
+ const input = value === void 0 ? fallback : plainObject(value, path);
2947
+ if (value !== void 0) {
2948
+ for (const key in input) if (key !== "x" && key !== "y") throw new TypeError(`${path}.${key} is unknown`);
2949
+ }
2950
+ const inputX = finite(input.x, `${path}.x`);
2951
+ const inputY = finite(input.y, `${path}.y`);
2952
+ const scale = Math.max(Math.abs(inputX), Math.abs(inputY));
2953
+ if (!scale) throw new RangeError(`${path} must be nonzero`);
2954
+ const x = inputX / scale;
2955
+ const y = inputY / scale;
2956
+ const magnitude = Math.hypot(x, y);
2957
+ result.x = x / magnitude;
2958
+ result.y = y / magnitude;
2959
+ return result;
2960
+ };
2961
+ var direction = (value, fallback, path) => directionInto(value, fallback, path, {});
2962
+ var newPose = () => ({
2963
+ head: {
2964
+ x: 0,
2965
+ y: 0
2966
+ },
2967
+ center: {
2968
+ x: 0,
2969
+ y: 0
2970
+ },
2971
+ direction: {
2972
+ x: 0,
2973
+ y: 0
2974
+ }
2975
+ });
2976
+ var bodyFitsWorld = (body) => body.chain.fitsTranslation({
2977
+ x: 0,
2978
+ y: 0
2979
+ }, MAX_WORLD_COORDINATE);
2980
+ var sameTopology = (before, after) => [
2981
+ "head",
2982
+ "trunk",
2983
+ "tail"
2984
+ ].every((name) => before.sections[name].count === after.sections[name].count);
2985
+ var legCountKey = (model) => model.descriptor.legs.pairs;
2986
+ var ornamentKey = (model) => model.skin.ornaments.map((ornament) => `${ornament.id}:${ornament.side}`).join("|");
2987
+ var Beefwife = class extends Container {
2988
+ #random;
2989
+ #requestedDirection;
2990
+ #model;
2991
+ #gait;
2992
+ #body;
2993
+ #legs;
2994
+ #skin;
2995
+ #graphics = null;
2996
+ #gone = false;
2997
+ #renderOptions = null;
2998
+ #renderState = null;
2999
+ #pose = newPose();
3000
+ #throttle;
3001
+ #stepThrottle;
3002
+ #updateDependents = (seconds) => {
3003
+ this.#throttle = this.#stepThrottle;
3004
+ this.#legs.update(seconds, this.#stepThrottle);
3005
+ this.#skin.update(seconds);
3006
+ };
3007
+ constructor(descriptor, rawOptions) {
3008
+ super();
3009
+ const options = optionsOf(rawOptions, OPTION_KEYS, "options");
3010
+ const position = worldPoint(options.position, {
3011
+ x: 0,
3012
+ y: 0
3013
+ }, "options.position");
3014
+ const facing = direction(options.direction, {
3015
+ x: 1,
3016
+ y: 0
3017
+ }, "options.direction");
3018
+ const phase = finite(defaulted(options.phase, 0), "options.phase");
3019
+ if (options.random !== void 0 && typeof options.random !== "function") throw new TypeError("options.random must be a function");
3020
+ this.#random = options.random ?? Math.random;
3021
+ this.#renderOptions = renderOptionsOf(options.render);
3022
+ this.#requestedDirection = { ...facing };
3023
+ this.#model = modelFor(descriptor);
3024
+ graphics_default.prepareAtlas(this.#model, this.#renderOptions);
3025
+ this.#gait = new Gait(this.#model.gait, phase);
3026
+ const breathingPhase = this.#model.breathing.strain ? TAU * this.#sampleRandom() : this.#gait.phase;
3027
+ this.#body = new Body(this.#model, this.#gait, breathingPhase);
3028
+ this.#body.place(position, facing);
3029
+ if (!bodyFitsWorld(this.#body)) throw new RangeError("options.position places the body outside the world");
3030
+ this.#legs = new Legs(this.#model, this.#body, this.#gait, () => this.#sampleRandom());
3031
+ this.#throttle = 1;
3032
+ this.#stepThrottle = 1;
3033
+ this.#skin = new Skin(this.#model, this.#body, this.#legs);
3034
+ this.#refreshPose();
3035
+ this.label = this.#model.descriptor.name;
3036
+ this.onRender = graphics_default.available ? (renderer) => this.#syncGraphics(renderer) : null;
3037
+ this.#replaceGraphics();
3038
+ }
3039
+ get descriptor() {
3040
+ return this.#model.descriptor;
3041
+ }
3042
+ get restLength() {
3043
+ return this.#model.restLength;
3044
+ }
3045
+ step(rawDt, rawControls) {
3046
+ this.#live("step");
3047
+ const dt = finite(rawDt, "dt");
3048
+ if (dt < 0) throw new RangeError("dt must be nonnegative");
3049
+ const controls = optionsOf(rawControls, CONTROL_KEYS, "controls");
3050
+ const throttle = finite(defaulted(controls.throttle, 1), "controls.throttle");
3051
+ if (throttle < 0 || throttle > 1) throw new RangeError("controls.throttle must be from 0 to 1");
3052
+ const wanted = directionInto(controls.direction, this.#requestedDirection, "controls.direction", this.#requestedDirection);
3053
+ this.#stepThrottle = throttle;
3054
+ if (this.#body.step(dt, throttle, wanted, this.#updateDependents)) {
3055
+ const correction = this.#body.chain.worldCorrection(MAX_WORLD_COORDINATE);
3056
+ if (correction.x || correction.y) {
3057
+ this.#body.chain.translate(correction);
3058
+ this.#legs.translate(correction);
3059
+ this.#skin.translate(correction);
3060
+ }
3061
+ this.#refreshPose();
3062
+ }
3063
+ }
3064
+ setDescriptor(descriptor) {
3065
+ this.#live("setDescriptor");
3066
+ const nextModel = modelFor(descriptor);
3067
+ graphics_default.prepareAtlas(nextModel, this.#renderOptions);
3068
+ const nextGait = new Gait(nextModel.gait, this.#gait.phase);
3069
+ const breathingPhase = !this.#model.breathing.strain && nextModel.breathing.strain ? TAU * this.#sampleRandom() : this.#body.breathingPhase;
3070
+ const compatible = sameTopology(this.#model, nextModel);
3071
+ let body = this.#body;
3072
+ if (!compatible) {
3073
+ body = new Body(nextModel, nextGait, breathingPhase);
3074
+ body.adopt(this.#body);
3075
+ body.refreshContacts(this.#throttle);
3076
+ if (!bodyFitsWorld(body)) throw new RangeError("descriptor places the body outside the world");
3077
+ }
3078
+ const nextLegs = legCountKey(this.#model) === legCountKey(nextModel) ? null : new Legs(nextModel, body, nextGait, () => this.#sampleRandom());
3079
+ const nextSkin = ornamentKey(this.#model) === ornamentKey(nextModel) ? null : new Skin(nextModel, body, nextLegs || this.#legs);
3080
+ if (compatible) this.#body.reconfigure(nextModel, nextGait, this.#throttle, breathingPhase);
3081
+ else this.#body = body;
3082
+ if (nextLegs) this.#legs = nextLegs;
3083
+ else this.#legs.reconfigure(nextModel, this.#body, nextGait);
3084
+ if (nextSkin) this.#skin = nextSkin;
3085
+ else this.#skin.reconfigure(nextModel, this.#body, this.#legs);
3086
+ this.#model = nextModel;
3087
+ this.#gait = nextGait;
3088
+ this.#refreshPose();
3089
+ this.label = nextModel.descriptor.name;
3090
+ this.#replaceGraphics();
3091
+ }
3092
+ reset(rawOptions) {
3093
+ this.#live("reset");
3094
+ const options = optionsOf(rawOptions, RESET_KEYS, "options");
3095
+ const pose = this.#body.getPose(this.#pose);
3096
+ const position = worldPoint(options.position, pose.head, "options.position");
3097
+ const facing = direction(options.direction, pose.direction, "options.direction");
3098
+ const phase = finite(defaulted(options.phase, this.#gait.phase), "options.phase");
3099
+ const gait = new Gait(this.#model.gait, phase);
3100
+ const breathingPhase = this.#model.breathing.strain ? TAU * this.#sampleRandom() : gait.phase;
3101
+ const body = new Body(this.#model, gait, breathingPhase);
3102
+ body.place(position, facing);
3103
+ if (!bodyFitsWorld(body)) throw new RangeError("options.position places the body outside the world");
3104
+ const legs = new Legs(this.#model, body, gait, () => this.#sampleRandom());
3105
+ const skin = new Skin(this.#model, body, legs);
3106
+ if (options.direction !== void 0) this.#requestedDirection = { ...facing };
3107
+ this.#gait = gait;
3108
+ this.#body = body;
3109
+ this.#legs = legs;
3110
+ this.#skin = skin;
3111
+ this.#throttle = 1;
3112
+ this.#refreshPose();
3113
+ if (this.#graphics) this.#syncGraphics();
3114
+ }
3115
+ translate(rawOffset) {
3116
+ this.#live("translate");
3117
+ const offset = worldPoint(rawOffset, null, "offset");
3118
+ if (!this.#body.chain.fitsTranslation(offset, 1e9)) throw new RangeError("offset places the body outside the world");
3119
+ this.#body.chain.translate(offset);
3120
+ this.#legs.translate(offset);
3121
+ this.#skin.translate(offset);
3122
+ this.#refreshPose();
3123
+ }
3124
+ getPose() {
3125
+ return this.#pose;
3126
+ }
3127
+ getBendResponse(into) {
3128
+ this.#live("getBendResponse");
3129
+ return this.#body.bend.response(this.#body.chain, into);
3130
+ }
3131
+ destroy(options) {
3132
+ this.onRender = null;
3133
+ if (this.#graphics) {
3134
+ this.#graphics.destroy();
3135
+ this.#graphics = null;
3136
+ }
3137
+ this.#gone = true;
3138
+ super.destroy(options);
3139
+ }
3140
+ #live(method) {
3141
+ if (this.#gone) throw new Error(`${method} was called on a destroyed beefwife`);
3142
+ }
3143
+ #sampleRandom() {
3144
+ const sample = this.#random();
3145
+ if (typeof sample !== "number" || !Number.isFinite(sample)) throw new TypeError("random() must return a finite number");
3146
+ if (sample < 0 || sample >= 1) throw new RangeError("random() must return a number from 0 to 1");
3147
+ return sample;
3148
+ }
3149
+ #refreshPose() {
3150
+ this.#body.getPose(this.#pose);
3151
+ }
3152
+ #replaceGraphics() {
3153
+ this.#renderState = this.#skin.writeRenderState(this.#renderState);
3154
+ if (this.#graphics) this.#graphics.adopt(this.#renderState);
3155
+ else if (graphics_default.available) this.#graphics = new graphics_default(this, this.#renderState, this.#renderOptions);
3156
+ }
3157
+ #syncGraphics(renderer = null) {
3158
+ const drawable = this.visible && this.renderable;
3159
+ if (drawable) this.#renderState = this.#skin.writeRenderState(this.#renderState);
3160
+ this.#graphics.sync(this.#renderState, renderer, drawable);
3161
+ }
3162
+ };
3163
+ Object.defineProperty(Beefwife, "MAX_WORLD_COORDINATE", {
3164
+ value: MAX_WORLD_COORDINATE,
3165
+ enumerable: true
3166
+ });
3167
+
3168
+ // src/global.mjs
3169
+ /**
3170
+ * Classic-script entry. A page gets one global, `Beefwife`, and reaches the
3171
+ * JSON half through `Beefwife.Descriptor`. Module consumers import both by
3172
+ * name, so the namespace is attached only here.
3173
+ */
3174
+ Object.defineProperty(Beefwife, "Descriptor", {
3175
+ value: descriptor_exports,
3176
+ enumerable: true
3177
+ });
3178
+ var global_default = Beefwife;
3179
+
3180
+ return global_default;
3181
+ })(globalThis.PIXI);
3182
+ if (typeof module !== "undefined" && module.exports) module.exports = Beefwife;