scena3d 0.1.0 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.cjs CHANGED
@@ -21,18 +21,34 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
21
21
  var index_exports = {};
22
22
  __export(index_exports, {
23
23
  DEFAULT_PALETTE: () => DEFAULT_PALETTE,
24
+ KIT_UNIT: () => KIT_UNIT,
24
25
  PALETTES: () => PALETTES,
25
26
  Rng: () => Rng,
27
+ aboveWater: () => aboveWater,
26
28
  applyFog: () => applyFog,
29
+ applyWind: () => applyWind,
30
+ assembleKit: () => assembleKit,
31
+ buildScene: () => buildScene,
27
32
  collectObstacles: () => collectObstacles,
33
+ createBush: () => createBush,
28
34
  createCrate: () => createCrate,
35
+ createDayCycle: () => createDayCycle,
29
36
  createFence: () => createFence,
37
+ createGrassTuft: () => createGrassTuft,
38
+ createHouse: () => createHouse,
30
39
  createLamp: () => createLamp,
31
40
  createLightingRig: () => createLightingRig,
41
+ createPath: () => createPath,
32
42
  createRock: () => createRock,
43
+ createRuin: () => createRuin,
33
44
  createSky: () => createSky,
34
45
  createTerrain: () => createTerrain,
46
+ createTower: () => createTower,
35
47
  createTree: () => createTree,
48
+ createVillage: () => createVillage,
49
+ createWater: () => createWater,
50
+ createWell: () => createWell,
51
+ extractMarkers: () => extractMarkers,
36
52
  fractalNoise2: () => fractalNoise2,
37
53
  hash2: () => hash2,
38
54
  scatter: () => scatter,
@@ -121,7 +137,12 @@ var PALETTES = {
121
137
  peak: 15265007,
122
138
  skyTop: 4026552,
123
139
  skyBottom: 12573160,
124
- fog: 12111837
140
+ fog: 12111837,
141
+ water: 4161454,
142
+ sand: 13220234,
143
+ path: 10125663,
144
+ wall: 14273712,
145
+ roof: 11032126
125
146
  },
126
147
  autumn: {
127
148
  foliage: [13202735, 14257722, 11754538, 14722373],
@@ -137,7 +158,12 @@ var PALETTES = {
137
158
  peak: 14933716,
138
159
  skyTop: 9333928,
139
160
  skyBottom: 15255976,
140
- fog: 14270888
161
+ fog: 14270888,
162
+ water: 4881042,
163
+ sand: 13348995,
164
+ path: 9270356,
165
+ wall: 13416596,
166
+ roof: 9062960
141
167
  },
142
168
  dusk: {
143
169
  foliage: [2055750, 2385999, 1724992, 2978904],
@@ -153,7 +179,33 @@ var PALETTES = {
153
179
  peak: 12105945,
154
180
  skyTop: 1909061,
155
181
  skyBottom: 13199946,
156
- fog: 6969978
182
+ fog: 6969978,
183
+ water: 2968168,
184
+ sand: 9075306,
185
+ path: 6969938,
186
+ wall: 9274009,
187
+ roof: 4535640
188
+ },
189
+ winter: {
190
+ foliage: [3038280, 3696986, 5405288, 8889750],
191
+ trunk: 4864563,
192
+ rock: [10134701, 8687256, 11581632],
193
+ wood: 7823433,
194
+ woodDark: 5522746,
195
+ metal: 3752013,
196
+ lampGlow: 16767113,
197
+ grassLow: 13621726,
198
+ grassHigh: 15002606,
199
+ cliff: 7764349,
200
+ peak: 16054266,
201
+ skyTop: 5929894,
202
+ skyBottom: 14214380,
203
+ fog: 13424864,
204
+ water: 4878470,
205
+ sand: 12108486,
206
+ path: 9143160,
207
+ wall: 13814203,
208
+ roof: 7030328
157
209
  }
158
210
  };
159
211
  var DEFAULT_PALETTE = PALETTES.meadow;
@@ -368,8 +420,321 @@ function createLamp(options = {}) {
368
420
  return { object: group, obstacleRadius: 0.25 };
369
421
  }
370
422
 
371
- // src/environment/terrain.ts
423
+ // src/props/grass.ts
372
424
  var import_three7 = require("three");
425
+ function createGrassTuft(options = {}) {
426
+ const rng = new Rng(options.seed ?? 1);
427
+ const palette = options.palette ?? DEFAULT_PALETTE;
428
+ const blades = options.blades ?? rng.int(4, 6);
429
+ const group = new import_three7.Group();
430
+ group.name = "grass";
431
+ const material = new import_three7.MeshStandardMaterial({
432
+ color: rng.next() < 0.5 ? palette.grassHigh : rng.pick(palette.foliage),
433
+ flatShading: true
434
+ });
435
+ for (let i = 0; i < blades; i++) {
436
+ const height = rng.range(0.25, 0.5);
437
+ const blade = new import_three7.Mesh(new import_three7.ConeGeometry(0.035, height, 3), material);
438
+ blade.position.set(rng.jitter(0, 0.12), height / 2, rng.jitter(0, 0.12));
439
+ blade.rotation.set(rng.range(-0.25, 0.25), rng.range(0, Math.PI), rng.range(-0.25, 0.25));
440
+ group.add(blade);
441
+ }
442
+ return { object: group, obstacleRadius: 0 };
443
+ }
444
+ function createBush(options = {}) {
445
+ const rng = new Rng(options.seed ?? 1);
446
+ const palette = options.palette ?? DEFAULT_PALETTE;
447
+ const size = options.size ?? rng.range(0.4, 0.7);
448
+ const group = new import_three7.Group();
449
+ group.name = "bush";
450
+ const material = new import_three7.MeshStandardMaterial({ color: rng.pick(palette.foliage), flatShading: true });
451
+ const blobs = rng.int(2, 3);
452
+ for (let i = 0; i < blobs; i++) {
453
+ const radius = size * rng.range(0.55, 0.85);
454
+ const blob = new import_three7.Mesh(new import_three7.IcosahedronGeometry(radius, 0), material);
455
+ blob.position.set(rng.jitter(0, size * 0.4), radius * 0.7, rng.jitter(0, size * 0.4));
456
+ blob.scale.y = rng.range(0.7, 0.85);
457
+ blob.rotation.y = rng.range(0, Math.PI);
458
+ group.add(blob);
459
+ }
460
+ return { object: group, obstacleRadius: size * 0.6 };
461
+ }
462
+
463
+ // src/props/building.ts
464
+ var import_three8 = require("three");
465
+ function prismGeometry(width, height, depth) {
466
+ const w = width / 2;
467
+ const d = depth / 2;
468
+ const positions = new Float32Array([
469
+ // front gable (z = +d)
470
+ -w,
471
+ 0,
472
+ d,
473
+ w,
474
+ 0,
475
+ d,
476
+ 0,
477
+ height,
478
+ d,
479
+ // back gable (z = -d)
480
+ w,
481
+ 0,
482
+ -d,
483
+ -w,
484
+ 0,
485
+ -d,
486
+ 0,
487
+ height,
488
+ -d,
489
+ // left slope
490
+ -w,
491
+ 0,
492
+ -d,
493
+ -w,
494
+ 0,
495
+ d,
496
+ 0,
497
+ height,
498
+ d,
499
+ -w,
500
+ 0,
501
+ -d,
502
+ 0,
503
+ height,
504
+ d,
505
+ 0,
506
+ height,
507
+ -d,
508
+ // right slope
509
+ w,
510
+ 0,
511
+ d,
512
+ w,
513
+ 0,
514
+ -d,
515
+ 0,
516
+ height,
517
+ -d,
518
+ w,
519
+ 0,
520
+ d,
521
+ 0,
522
+ height,
523
+ -d,
524
+ 0,
525
+ height,
526
+ d,
527
+ // bottom
528
+ -w,
529
+ 0,
530
+ -d,
531
+ w,
532
+ 0,
533
+ -d,
534
+ w,
535
+ 0,
536
+ d,
537
+ -w,
538
+ 0,
539
+ -d,
540
+ w,
541
+ 0,
542
+ d,
543
+ -w,
544
+ 0,
545
+ d
546
+ ]);
547
+ const geometry = new import_three8.BufferGeometry();
548
+ geometry.setAttribute("position", new import_three8.BufferAttribute(positions, 3));
549
+ geometry.computeVertexNormals();
550
+ return geometry;
551
+ }
552
+ function createHouse(options = {}) {
553
+ const rng = new Rng(options.seed ?? 1);
554
+ const palette = options.palette ?? DEFAULT_PALETTE;
555
+ const width = options.width ?? rng.range(3.2, 4.2);
556
+ const depth = options.depth ?? width * rng.range(0.75, 0.9);
557
+ const wallHeight = options.wallHeight ?? rng.range(2.1, 2.4);
558
+ const group = new import_three8.Group();
559
+ group.name = "house";
560
+ const wall = new import_three8.MeshStandardMaterial({ color: palette.wall, flatShading: true });
561
+ wall.color.offsetHSL(0, 0, rng.range(-0.03, 0.03));
562
+ const roof = new import_three8.MeshStandardMaterial({ color: palette.roof, flatShading: true });
563
+ roof.color.offsetHSL(0, 0, rng.range(-0.04, 0.04));
564
+ const stone = new import_three8.MeshStandardMaterial({ color: palette.rock[0], flatShading: true });
565
+ const wood = new import_three8.MeshStandardMaterial({ color: palette.woodDark, flatShading: true });
566
+ const foundation = new import_three8.Mesh(new import_three8.BoxGeometry(width + 0.3, 1.6, depth + 0.3), stone);
567
+ foundation.position.y = -0.55;
568
+ group.add(foundation);
569
+ const body = new import_three8.Mesh(new import_three8.BoxGeometry(width, wallHeight, depth), wall);
570
+ body.position.y = wallHeight / 2;
571
+ group.add(body);
572
+ const ridgeHeight = width * rng.range(0.32, 0.4);
573
+ const roofMesh = new import_three8.Mesh(prismGeometry(width + 0.5, ridgeHeight, depth + 0.6), roof);
574
+ roofMesh.position.y = wallHeight - 0.02;
575
+ group.add(roofMesh);
576
+ const chimney = new import_three8.Mesh(new import_three8.BoxGeometry(0.34, ridgeHeight + 0.8, 0.34), stone);
577
+ chimney.position.set(rng.pick([-1, 1]) * width * 0.22, wallHeight + ridgeHeight * 0.45, depth * 0.12);
578
+ group.add(chimney);
579
+ const door = new import_three8.Mesh(new import_three8.BoxGeometry(0.85, 1.5, 0.08), wood);
580
+ door.position.set(rng.range(-0.4, 0.4), 0.75, depth / 2 + 0.02);
581
+ group.add(door);
582
+ const glass = new import_three8.MeshStandardMaterial({
583
+ color: palette.lampGlow,
584
+ emissive: palette.lampGlow,
585
+ emissiveIntensity: 1
586
+ });
587
+ const windowGeometry = new import_three8.BoxGeometry(0.6, 0.62, 0.08);
588
+ const front = new import_three8.Mesh(windowGeometry, glass);
589
+ front.position.set(door.position.x < 0 ? width * 0.28 : -width * 0.28, 1.35, depth / 2 + 0.02);
590
+ group.add(front);
591
+ for (const side of [-1, 1]) {
592
+ if (rng.next() < 0.35) continue;
593
+ const pane = new import_three8.Mesh(windowGeometry, glass);
594
+ pane.position.set(side * (width / 2 + 0.02), 1.35, rng.range(-0.3, 0.3) * depth);
595
+ pane.rotation.y = Math.PI / 2;
596
+ group.add(pane);
597
+ }
598
+ return { object: group, obstacleRadius: Math.hypot(width + 0.5, depth + 0.5) / 2 };
599
+ }
600
+ function createTower(options = {}) {
601
+ const rng = new Rng(options.seed ?? 1);
602
+ const palette = options.palette ?? DEFAULT_PALETTE;
603
+ const height = options.height ?? rng.range(4.2, 5.2);
604
+ const group = new import_three8.Group();
605
+ group.name = "tower";
606
+ const wood = new import_three8.MeshStandardMaterial({ color: palette.wood, flatShading: true });
607
+ const woodDark = new import_three8.MeshStandardMaterial({ color: palette.woodDark, flatShading: true });
608
+ const roof = new import_three8.MeshStandardMaterial({ color: palette.roof, flatShading: true });
609
+ const spread = 1;
610
+ for (const x of [-1, 1]) {
611
+ for (const z of [-1, 1]) {
612
+ const leg = new import_three8.Mesh(new import_three8.CylinderGeometry(0.09, 0.12, height, 6), woodDark);
613
+ leg.position.set(x * spread * 0.75, height / 2 - 0.6, z * spread * 0.75);
614
+ leg.rotation.z = -x * 0.1;
615
+ leg.rotation.x = z * 0.1;
616
+ group.add(leg);
617
+ }
618
+ }
619
+ for (const level of [0.3, 0.62]) {
620
+ const brace = new import_three8.Mesh(new import_three8.BoxGeometry(spread * 2.1, 0.09, 0.07), wood);
621
+ brace.position.set(0, height * level, spread * 0.82 * (1 - level * 0.35));
622
+ brace.rotation.z = rng.pick([-0.5, 0.5]);
623
+ group.add(brace);
624
+ const braceSide = new import_three8.Mesh(new import_three8.BoxGeometry(0.07, 0.09, spread * 2.1), wood);
625
+ braceSide.position.set(spread * 0.82 * (1 - level * 0.35), height * level, 0);
626
+ braceSide.rotation.x = rng.pick([-0.5, 0.5]);
627
+ group.add(braceSide);
628
+ }
629
+ const platform = new import_three8.Mesh(new import_three8.BoxGeometry(spread * 2.2, 0.14, spread * 2.2), wood);
630
+ platform.position.y = height - 0.5;
631
+ group.add(platform);
632
+ for (const x of [-1, 1]) {
633
+ for (const z of [-1, 1]) {
634
+ const post = new import_three8.Mesh(new import_three8.BoxGeometry(0.08, 0.9, 0.08), woodDark);
635
+ post.position.set(x * spread * 1, height - 0.05, z * spread * 1);
636
+ group.add(post);
637
+ }
638
+ }
639
+ for (const [rx, rz, w, d] of [
640
+ [0, 1, spread * 2.1, 0.06],
641
+ [0, -1, spread * 2.1, 0.06],
642
+ [1, 0, 0.06, spread * 2.1],
643
+ [-1, 0, 0.06, spread * 2.1]
644
+ ]) {
645
+ const rail = new import_three8.Mesh(new import_three8.BoxGeometry(w, 0.07, d), wood);
646
+ rail.position.set(rx * spread, height + 0.28, rz * spread);
647
+ group.add(rail);
648
+ }
649
+ const cap = new import_three8.Mesh(new import_three8.CylinderGeometry(0, spread * 1.55, 1, 4), roof);
650
+ cap.position.y = height + 1.15;
651
+ cap.rotation.y = Math.PI / 4;
652
+ group.add(cap);
653
+ return { object: group, obstacleRadius: spread * 1.35 };
654
+ }
655
+ function createWell(options = {}) {
656
+ const rng = new Rng(options.seed ?? 1);
657
+ const palette = options.palette ?? DEFAULT_PALETTE;
658
+ const group = new import_three8.Group();
659
+ group.name = "well";
660
+ const stone = new import_three8.MeshStandardMaterial({ color: rng.pick(palette.rock), flatShading: true });
661
+ const wood = new import_three8.MeshStandardMaterial({ color: palette.woodDark, flatShading: true });
662
+ const roof = new import_three8.MeshStandardMaterial({ color: palette.roof, flatShading: true });
663
+ const ring = new import_three8.Mesh(new import_three8.CylinderGeometry(0.85, 0.95, 0.75, 10), stone);
664
+ ring.position.y = 0.375;
665
+ group.add(ring);
666
+ const inner = new import_three8.Mesh(
667
+ new import_three8.CylinderGeometry(0.62, 0.62, 0.05, 10),
668
+ new import_three8.MeshStandardMaterial({ color: 1450542 })
669
+ );
670
+ inner.position.y = 0.76;
671
+ group.add(inner);
672
+ for (const side of [-1, 1]) {
673
+ const post = new import_three8.Mesh(new import_three8.CylinderGeometry(0.06, 0.075, 1.9, 5), wood);
674
+ post.position.set(side * 0.72, 0.95, 0);
675
+ group.add(post);
676
+ }
677
+ const bar = new import_three8.Mesh(new import_three8.CylinderGeometry(0.045, 0.045, 1.5, 5), wood);
678
+ bar.rotation.z = Math.PI / 2;
679
+ bar.position.y = 1.72;
680
+ group.add(bar);
681
+ const cap = new import_three8.Mesh(prismGeometry(2, 0.55, 0.65), roof);
682
+ cap.position.y = 1.86;
683
+ cap.rotation.y = Math.PI / 2;
684
+ group.add(cap);
685
+ const rope = new import_three8.Mesh(new import_three8.CylinderGeometry(0.012, 0.012, 0.6, 4), wood);
686
+ rope.position.y = 1.42;
687
+ group.add(rope);
688
+ const bucket = new import_three8.Mesh(new import_three8.CylinderGeometry(0.13, 0.1, 0.2, 7), wood);
689
+ bucket.position.y = 1.05;
690
+ group.add(bucket);
691
+ return { object: group, obstacleRadius: 1 };
692
+ }
693
+ function createRuin(options = {}) {
694
+ const rng = new Rng(options.seed ?? 1);
695
+ const palette = options.palette ?? DEFAULT_PALETTE;
696
+ const size = options.size ?? rng.range(3.5, 5);
697
+ const depth = size * rng.range(0.7, 0.9);
698
+ const group = new import_three8.Group();
699
+ group.name = "ruin";
700
+ const stone = new import_three8.MeshStandardMaterial({ color: rng.pick(palette.rock), flatShading: true });
701
+ stone.color.offsetHSL(0, 0, rng.range(-0.04, 0.02));
702
+ const thickness = 0.35;
703
+ const runs = [
704
+ [0, -depth / 2, size, true],
705
+ [-size / 2, 0, depth, false],
706
+ [size / 2, 0, depth, false],
707
+ [size * 0.3, depth / 2, size * 0.35, true]
708
+ ];
709
+ for (const [cx, cz, length, alongX] of runs) {
710
+ const segments = Math.max(2, Math.round(length / 1.1));
711
+ const step = length / segments;
712
+ for (let i = 0; i < segments; i++) {
713
+ if (rng.next() < 0.22) continue;
714
+ const height = rng.range(0.5, 2.2);
715
+ const wall = new import_three8.Mesh(
716
+ new import_three8.BoxGeometry(alongX ? step * 0.96 : thickness, height, alongX ? thickness : step * 0.96),
717
+ stone
718
+ );
719
+ const offset = -length / 2 + step * (i + 0.5);
720
+ wall.position.set(alongX ? cx + offset : cx, height / 2, alongX ? cz : cz + offset);
721
+ wall.rotation.y = rng.range(-0.03, 0.03);
722
+ group.add(wall);
723
+ }
724
+ }
725
+ const blocks = rng.int(4, 7);
726
+ for (let i = 0; i < blocks; i++) {
727
+ const s = rng.range(0.25, 0.55);
728
+ const block = new import_three8.Mesh(new import_three8.BoxGeometry(s, s * rng.range(0.6, 1), s * rng.range(0.7, 1.2)), stone);
729
+ block.position.set(rng.range(-size * 0.6, size * 0.6), s * 0.3, rng.range(-depth * 0.6, depth * 0.7));
730
+ block.rotation.set(rng.range(-0.3, 0.3), rng.range(0, Math.PI), rng.range(-0.3, 0.3));
731
+ group.add(block);
732
+ }
733
+ return { object: group, obstacleRadius: Math.hypot(size, depth) / 2 };
734
+ }
735
+
736
+ // src/environment/terrain.ts
737
+ var import_three9 = require("three");
373
738
  function createTerrain(options = {}) {
374
739
  const seed = options.seed ?? 1;
375
740
  const size = options.size ?? 80;
@@ -384,7 +749,7 @@ function createTerrain(options = {}) {
384
749
  const shaped = Math.pow(n, 1 + flatness * 2);
385
750
  return shaped * amplitude;
386
751
  };
387
- const geometry = new import_three7.PlaneGeometry(size, size, resolution - 1, resolution - 1);
752
+ const geometry = new import_three9.PlaneGeometry(size, size, resolution - 1, resolution - 1);
388
753
  geometry.rotateX(-Math.PI / 2);
389
754
  const positions = geometry.getAttribute("position");
390
755
  for (let i = 0; i < positions.count; i++) {
@@ -393,40 +758,46 @@ function createTerrain(options = {}) {
393
758
  geometry.computeVertexNormals();
394
759
  const normals = geometry.getAttribute("normal");
395
760
  const colors = new Float32Array(positions.count * 3);
396
- const grassLow = new import_three7.Color(palette.grassLow);
397
- const grassHigh = new import_three7.Color(palette.grassHigh);
398
- const cliff = new import_three7.Color(palette.cliff);
399
- const peak = new import_three7.Color(palette.peak);
400
- const scratch = new import_three7.Color();
761
+ const grassLow = new import_three9.Color(palette.grassLow);
762
+ const grassHigh = new import_three9.Color(palette.grassHigh);
763
+ const cliff = new import_three9.Color(palette.cliff);
764
+ const peak = new import_three9.Color(palette.peak);
765
+ const sand = new import_three9.Color(palette.sand);
766
+ const waterLevel = options.waterLevel;
767
+ const scratch = new import_three9.Color();
401
768
  for (let i = 0; i < positions.count; i++) {
402
- const h = positions.getY(i) / amplitude;
769
+ const y = positions.getY(i);
770
+ const h = y / amplitude;
403
771
  const slope = 1 - normals.getY(i);
404
772
  scratch.copy(grassLow).lerp(grassHigh, Math.min(1, h * 1.6));
405
773
  if (h > 0.75) scratch.lerp(peak, (h - 0.75) * 4);
406
774
  if (slope > 0.15) scratch.lerp(cliff, Math.min(1, (slope - 0.15) * 4));
775
+ if (waterLevel !== void 0 && y < waterLevel + 0.5) {
776
+ scratch.lerp(sand, Math.min(1, (waterLevel + 0.5 - y) * 1.6));
777
+ }
407
778
  colors[i * 3] = scratch.r;
408
779
  colors[i * 3 + 1] = scratch.g;
409
780
  colors[i * 3 + 2] = scratch.b;
410
781
  }
411
- geometry.setAttribute("color", new import_three7.BufferAttribute(colors, 3));
412
- const mesh = new import_three7.Mesh(
782
+ geometry.setAttribute("color", new import_three9.BufferAttribute(colors, 3));
783
+ const mesh = new import_three9.Mesh(
413
784
  geometry,
414
- new import_three7.MeshStandardMaterial({ vertexColors: true, flatShading: true })
785
+ new import_three9.MeshStandardMaterial({ vertexColors: true, flatShading: true })
415
786
  );
416
787
  mesh.name = "terrain";
417
788
  return { mesh, heightAt, size, seed };
418
789
  }
419
790
 
420
791
  // src/environment/sky.ts
421
- var import_three8 = require("three");
792
+ var import_three10 = require("three");
422
793
  function createSky(options = {}) {
423
794
  const palette = options.palette ?? DEFAULT_PALETTE;
424
- const material = new import_three8.ShaderMaterial({
425
- side: import_three8.BackSide,
795
+ const material = new import_three10.ShaderMaterial({
796
+ side: import_three10.BackSide,
426
797
  depthWrite: false,
427
798
  uniforms: {
428
- topColor: { value: new import_three8.Color(options.topColor ?? palette.skyTop) },
429
- bottomColor: { value: new import_three8.Color(options.bottomColor ?? palette.skyBottom) }
799
+ topColor: { value: new import_three10.Color(options.topColor ?? palette.skyTop) },
800
+ bottomColor: { value: new import_three10.Color(options.bottomColor ?? palette.skyBottom) }
430
801
  },
431
802
  vertexShader: (
432
803
  /* glsl */
@@ -449,7 +820,7 @@ function createSky(options = {}) {
449
820
  }`
450
821
  )
451
822
  });
452
- const mesh = new import_three8.Mesh(new import_three8.SphereGeometry(options.radius ?? 400, 16, 12), material);
823
+ const mesh = new import_three10.Mesh(new import_three10.SphereGeometry(options.radius ?? 400, 16, 12), material);
453
824
  mesh.name = "sky";
454
825
  return {
455
826
  mesh,
@@ -461,7 +832,7 @@ function createSky(options = {}) {
461
832
  }
462
833
 
463
834
  // src/environment/lighting.ts
464
- var import_three9 = require("three");
835
+ var import_three11 = require("three");
465
836
  var PRESETS = {
466
837
  day: {
467
838
  sun: 16774368,
@@ -502,12 +873,12 @@ var PRESETS = {
502
873
  };
503
874
  function createLightingRig(preset = "day") {
504
875
  const config = PRESETS[preset];
505
- const group = new import_three9.Group();
876
+ const group = new import_three11.Group();
506
877
  group.name = `lighting-${preset}`;
507
- const sun = new import_three9.DirectionalLight(config.sun, config.sunIntensity);
878
+ const sun = new import_three11.DirectionalLight(config.sun, config.sunIntensity);
508
879
  sun.position.set(...config.sunPos);
509
- const ambient = new import_three9.AmbientLight(config.ambient, config.ambientIntensity);
510
- const hemisphere = new import_three9.HemisphereLight(config.skyTint, config.groundTint, 0.5);
880
+ const ambient = new import_three11.AmbientLight(config.ambient, config.ambientIntensity);
881
+ const hemisphere = new import_three11.HemisphereLight(config.skyTint, config.groundTint, 0.5);
511
882
  group.add(sun, ambient, hemisphere);
512
883
  return { group, sun, ambient, hemisphere };
513
884
  }
@@ -522,11 +893,512 @@ function applyFog(scene, preset, palette = DEFAULT_PALETTE) {
522
893
  return;
523
894
  }
524
895
  const { near, far } = FOG[preset];
525
- scene.fog = new import_three9.Fog(palette.fog, near, far);
896
+ scene.fog = new import_three11.Fog(palette.fog, near, far);
897
+ }
898
+
899
+ // src/environment/water.ts
900
+ var import_three12 = require("three");
901
+ function createWater(options = {}) {
902
+ const level = options.level ?? 0.8;
903
+ const size = options.size ?? 200;
904
+ const resolution = options.resolution ?? 40;
905
+ const amplitude = options.amplitude ?? 0.06;
906
+ const speed = options.speed ?? 1;
907
+ const palette = options.palette ?? DEFAULT_PALETTE;
908
+ const geometry = new import_three12.PlaneGeometry(size, size, resolution, resolution);
909
+ geometry.rotateX(-Math.PI / 2);
910
+ const positions = geometry.getAttribute("position");
911
+ const mesh = new import_three12.Mesh(
912
+ geometry,
913
+ new import_three12.MeshStandardMaterial({
914
+ color: palette.water,
915
+ transparent: true,
916
+ opacity: 0.85,
917
+ flatShading: true,
918
+ metalness: 0.35,
919
+ roughness: 0.4
920
+ })
921
+ );
922
+ mesh.name = "water";
923
+ mesh.position.y = level;
924
+ let time = 0;
925
+ const update = (dt) => {
926
+ time += dt * speed;
927
+ for (let i = 0; i < positions.count; i++) {
928
+ const x = positions.getX(i);
929
+ const z = positions.getZ(i);
930
+ positions.setY(
931
+ i,
932
+ Math.sin(x * 0.35 + time) * Math.cos(z * 0.3 + time * 0.8) * amplitude
933
+ );
934
+ }
935
+ positions.needsUpdate = true;
936
+ geometry.computeVertexNormals();
937
+ };
938
+ update(0);
939
+ return {
940
+ mesh,
941
+ level,
942
+ update,
943
+ isUnderwater: (groundHeight) => groundHeight < level
944
+ };
945
+ }
946
+ function aboveWater(terrain, water, margin = 0.25) {
947
+ return (x, z) => terrain.heightAt(x, z) > water.level + margin;
948
+ }
949
+
950
+ // src/environment/dayCycle.ts
951
+ var import_three13 = require("three");
952
+ function createDayCycle(options = {}) {
953
+ const palette = options.palette ?? DEFAULT_PALETTE;
954
+ const dayLength = options.dayLength ?? 60;
955
+ const frames = [
956
+ { t: 0, skyTop: 725030, skyBottom: 1712960, sun: 9348824, sunIntensity: 0.03, ambientIntensity: 0.06, fog: 1317422 },
957
+ { t: 0.23, skyTop: 2569052, skyBottom: 6968432, sun: 13605490, sunIntensity: 0.3, ambientIntensity: 0.12, fog: 4866648 },
958
+ { t: 0.3, skyTop: 4877216, skyBottom: 15247738, sun: 16758881, sunIntensity: 1, ambientIntensity: 0.26, fog: 11569778 },
959
+ { t: 0.5, skyTop: palette.skyTop, skyBottom: palette.skyBottom, sun: 16774368, sunIntensity: 1.9, ambientIntensity: 0.45, fog: palette.fog },
960
+ { t: 0.7, skyTop: 4872852, skyBottom: 14718302, sun: 16752720, sunIntensity: 1, ambientIntensity: 0.26, fog: 10517096 },
961
+ { t: 0.78, skyTop: 2304594, skyBottom: 9065824, sun: 14191194, sunIntensity: 0.25, ambientIntensity: 0.11, fog: 4603476 },
962
+ { t: 1, skyTop: 725030, skyBottom: 1712960, sun: 9348824, sunIntensity: 0.03, ambientIntensity: 0.06, fog: 1317422 }
963
+ ];
964
+ const lampLights = [];
965
+ const lampBulbs = [];
966
+ for (const entry of options.lamps ?? []) {
967
+ const root = entry.isObject3D ? entry : entry.object;
968
+ root.traverse((child) => {
969
+ if (child instanceof import_three13.PointLight) {
970
+ lampLights.push({ light: child, base: child.intensity || 6 });
971
+ }
972
+ const material = child.material;
973
+ if (material?.emissive && material.emissiveIntensity > 0.5) {
974
+ lampBulbs.push({ material, base: material.emissiveIntensity });
975
+ }
976
+ });
977
+ }
978
+ const colorA = new import_three13.Color();
979
+ const colorB = new import_three13.Color();
980
+ let timeOfDay = options.timeOfDay ?? 0.5;
981
+ let sunElevation = 0;
982
+ const sample = (t) => {
983
+ let a = frames[0];
984
+ let b = frames[frames.length - 1];
985
+ for (let i = 0; i < frames.length - 1; i++) {
986
+ if (t >= frames[i].t && t <= frames[i + 1].t) {
987
+ a = frames[i];
988
+ b = frames[i + 1];
989
+ break;
990
+ }
991
+ }
992
+ const f = b.t === a.t ? 0 : (t - a.t) / (b.t - a.t);
993
+ const lerpHex = (ha, hb) => colorA.setHex(ha).lerp(colorB.setHex(hb), f).getHex();
994
+ return {
995
+ t,
996
+ skyTop: lerpHex(a.skyTop, b.skyTop),
997
+ skyBottom: lerpHex(a.skyBottom, b.skyBottom),
998
+ sun: lerpHex(a.sun, b.sun),
999
+ sunIntensity: a.sunIntensity + (b.sunIntensity - a.sunIntensity) * f,
1000
+ ambientIntensity: a.ambientIntensity + (b.ambientIntensity - a.ambientIntensity) * f,
1001
+ fog: lerpHex(a.fog, b.fog)
1002
+ };
1003
+ };
1004
+ const apply = () => {
1005
+ const t = timeOfDay;
1006
+ const frame = sample(t);
1007
+ const sunAngle = (t - 0.25) * Math.PI * 2;
1008
+ sunElevation = Math.sin(sunAngle);
1009
+ options.sky?.setColors(frame.skyTop, frame.skyBottom);
1010
+ if (options.rig) {
1011
+ const { sun, ambient, hemisphere } = options.rig;
1012
+ sun.color.setHex(frame.sun);
1013
+ sun.intensity = frame.sunIntensity;
1014
+ sun.position.set(Math.cos(sunAngle) * 40, Math.max(sunElevation, -0.2) * 45 + 6, 16);
1015
+ ambient.intensity = frame.ambientIntensity;
1016
+ hemisphere.intensity = frame.ambientIntensity * 1.4;
1017
+ }
1018
+ if (options.scene?.fog && "color" in options.scene.fog) {
1019
+ options.scene.fog.color.setHex(frame.fog);
1020
+ }
1021
+ const night = Math.min(1, Math.max(0, (0.06 - sunElevation) / 0.16));
1022
+ for (const { light, base } of lampLights) light.intensity = base * night;
1023
+ for (const { material, base } of lampBulbs) {
1024
+ material.emissiveIntensity = 0.15 * base + 0.85 * base * night;
1025
+ }
1026
+ };
1027
+ apply();
1028
+ return {
1029
+ get timeOfDay() {
1030
+ return timeOfDay;
1031
+ },
1032
+ set timeOfDay(t) {
1033
+ timeOfDay = (t % 1 + 1) % 1;
1034
+ apply();
1035
+ },
1036
+ get sunElevation() {
1037
+ return sunElevation;
1038
+ },
1039
+ get isNight() {
1040
+ return sunElevation < 0;
1041
+ },
1042
+ update(dt) {
1043
+ timeOfDay = (timeOfDay + dt / dayLength) % 1;
1044
+ apply();
1045
+ },
1046
+ set(t) {
1047
+ timeOfDay = (t % 1 + 1) % 1;
1048
+ apply();
1049
+ }
1050
+ };
526
1051
  }
527
1052
 
1053
+ // src/environment/wind.ts
1054
+ var import_three14 = require("three");
1055
+ function applyWind(target, options = {}) {
1056
+ const strength = options.strength ?? 0.06;
1057
+ const frequency = options.frequency ?? 1.2;
1058
+ const anchor = options.anchorHeight ?? 0.6;
1059
+ const time = { value: 0 };
1060
+ const patched = /* @__PURE__ */ new Set();
1061
+ target.traverse((child) => {
1062
+ if (!(child instanceof import_three14.Mesh)) return;
1063
+ const materials = Array.isArray(child.material) ? child.material : [child.material];
1064
+ for (const material of materials) {
1065
+ if (patched.has(material)) continue;
1066
+ patched.add(material);
1067
+ material.onBeforeCompile = (shader) => {
1068
+ shader.uniforms.uWindTime = time;
1069
+ shader.vertexShader = shader.vertexShader.replace(
1070
+ "#include <common>",
1071
+ `#include <common>
1072
+ uniform float uWindTime;`
1073
+ ).replace(
1074
+ "#include <begin_vertex>",
1075
+ `#include <begin_vertex>
1076
+ {
1077
+ float sway = max(position.y - ${anchor.toFixed(3)}, 0.0);
1078
+ float phase = 0.0;
1079
+ #ifdef USE_INSTANCING
1080
+ phase = instanceMatrix[3].x * 0.7 + instanceMatrix[3].z * 1.3;
1081
+ #endif
1082
+ float w = sin(uWindTime * ${(frequency * 2).toFixed(3)} + phase + position.y * 0.8);
1083
+ transformed.x += w * sway * ${strength.toFixed(4)} * 4.0;
1084
+ transformed.z += cos(uWindTime * ${(frequency * 1.4).toFixed(3)} + phase) * sway * ${strength.toFixed(4)} * 2.0;
1085
+ }`
1086
+ );
1087
+ };
1088
+ material.needsUpdate = true;
1089
+ }
1090
+ });
1091
+ return {
1092
+ update(dt) {
1093
+ time.value += dt;
1094
+ },
1095
+ materials: [...patched]
1096
+ };
1097
+ }
1098
+
1099
+ // src/environment/path.ts
1100
+ var import_three15 = require("three");
1101
+ function createPath(points, options = {}) {
1102
+ const width = options.width ?? 1.8;
1103
+ const surface = options.surface ?? 0;
1104
+ const heightAt = typeof surface === "number" ? () => surface : (x, z) => surface(x, z);
1105
+ const loop = options.loop ?? false;
1106
+ const palette = options.palette ?? DEFAULT_PALETTE;
1107
+ const controls = points.map((p) => new import_three15.Vector3(p.x, 0, "z" in p ? p.z : 0));
1108
+ const curve = new import_three15.CatmullRomCurve3(controls, loop, "centripetal");
1109
+ const length = curve.getLength();
1110
+ const samples = Math.max(8, Math.ceil(length * (options.samplesPerUnit ?? 1)));
1111
+ const route = [];
1112
+ for (let i = 0; i <= samples; i++) {
1113
+ if (loop && i === samples) break;
1114
+ const p = curve.getPoint(i / samples);
1115
+ route.push(new import_three15.Vector3(p.x, heightAt(p.x, p.z), p.z));
1116
+ }
1117
+ const edgeCount = loop ? route.length + 1 : route.length;
1118
+ const positions = new Float32Array(edgeCount * 2 * 3);
1119
+ const direction = new import_three15.Vector3();
1120
+ const perp = new import_three15.Vector3();
1121
+ for (let i = 0; i < edgeCount; i++) {
1122
+ const current = route[i % route.length];
1123
+ const previous = route[(i - 1 + route.length) % route.length];
1124
+ const next = route[(i + 1) % route.length];
1125
+ if (!loop && i === 0) direction.subVectors(next, current);
1126
+ else if (!loop && i === edgeCount - 1) direction.subVectors(current, previous);
1127
+ else direction.subVectors(next, previous);
1128
+ perp.set(-direction.z, 0, direction.x).normalize().multiplyScalar(width / 2);
1129
+ const left = i * 6;
1130
+ positions[left] = current.x - perp.x;
1131
+ positions[left + 1] = heightAt(current.x - perp.x, current.z - perp.z) + 0.05;
1132
+ positions[left + 2] = current.z - perp.z;
1133
+ positions[left + 3] = current.x + perp.x;
1134
+ positions[left + 4] = heightAt(current.x + perp.x, current.z + perp.z) + 0.05;
1135
+ positions[left + 5] = current.z + perp.z;
1136
+ }
1137
+ const indices = [];
1138
+ for (let i = 0; i < edgeCount - 1; i++) {
1139
+ const a = i * 2;
1140
+ indices.push(a, a + 1, a + 2, a + 1, a + 3, a + 2);
1141
+ }
1142
+ const geometry = new import_three15.BufferGeometry();
1143
+ geometry.setAttribute("position", new import_three15.BufferAttribute(positions, 3));
1144
+ geometry.setIndex(indices);
1145
+ geometry.computeVertexNormals();
1146
+ const mesh = new import_three15.Mesh(
1147
+ geometry,
1148
+ new import_three15.MeshStandardMaterial({
1149
+ color: palette.path,
1150
+ flatShading: true,
1151
+ polygonOffset: true,
1152
+ polygonOffsetFactor: -1
1153
+ })
1154
+ );
1155
+ mesh.name = "path";
1156
+ const keepOutMargin = options.keepOutMargin ?? 0.6;
1157
+ const keepOut = route.filter((_p, i) => i % 2 === 0 || i === route.length - 1).map((p) => ({ center: { x: p.x, z: p.z }, radius: width / 2 + keepOutMargin }));
1158
+ const half = width / 2;
1159
+ const contains = (x, z) => {
1160
+ const count = loop ? route.length : route.length - 1;
1161
+ for (let i = 0; i < count; i++) {
1162
+ const a = route[i];
1163
+ const b = route[(i + 1) % route.length];
1164
+ const abx = b.x - a.x;
1165
+ const abz = b.z - a.z;
1166
+ const lengthSq = abx * abx + abz * abz || 1e-9;
1167
+ const t = Math.max(0, Math.min(1, ((x - a.x) * abx + (z - a.z) * abz) / lengthSq));
1168
+ const dx = x - (a.x + abx * t);
1169
+ const dz = z - (a.z + abz * t);
1170
+ if (dx * dx + dz * dz <= half * half) return true;
1171
+ }
1172
+ return false;
1173
+ };
1174
+ return { mesh, route, keepOut, contains, loop };
1175
+ }
1176
+
1177
+ // src/generators/village.ts
1178
+ var import_three16 = require("three");
1179
+ function createVillage(options = {}) {
1180
+ const rng = new Rng(options.seed ?? 1);
1181
+ const palette = options.palette ?? DEFAULT_PALETTE;
1182
+ const center = options.center ?? { x: 0, z: 0 };
1183
+ const radius = options.radius ?? 12;
1184
+ const houseCount = options.houses ?? 5;
1185
+ const surface = options.surface ?? 0;
1186
+ const heightAt = typeof surface === "number" ? () => surface : (x, z) => surface(x, z);
1187
+ const group = new import_three16.Group();
1188
+ group.name = "village";
1189
+ const props = [];
1190
+ const lamps = [];
1191
+ const place = (prop, x, z, rotY) => {
1192
+ prop.object.position.set(x, heightAt(x, z), z);
1193
+ prop.object.rotation.y = rotY;
1194
+ group.add(prop.object);
1195
+ props.push(prop);
1196
+ return prop;
1197
+ };
1198
+ const findSpot = (angle, distance, footprint) => {
1199
+ let x = center.x;
1200
+ let z = center.z;
1201
+ for (let attempt = 0; attempt < 10; attempt++) {
1202
+ const a = angle + rng.range(-0.25, 0.25) * (attempt > 0 ? 1 : 0.3);
1203
+ const d = distance * rng.range(attempt > 0 ? 0.8 : 0.95, 1.1);
1204
+ x = center.x + Math.cos(a) * d;
1205
+ z = center.z + Math.sin(a) * d;
1206
+ const y = heightAt(x, z);
1207
+ if (options.mask && !options.mask(x, z, y)) continue;
1208
+ let min = y;
1209
+ let max = y;
1210
+ for (const [dx, dz] of [[footprint, 0], [-footprint, 0], [0, footprint], [0, -footprint]]) {
1211
+ const h = heightAt(x + dx, z + dz);
1212
+ min = Math.min(min, h);
1213
+ max = Math.max(max, h);
1214
+ }
1215
+ if (max - min <= Math.max(0.9, footprint * 0.35)) break;
1216
+ }
1217
+ return { x, z };
1218
+ };
1219
+ const well = createWell({ seed: rng.int(1, 1e9), palette });
1220
+ place(well, center.x, center.z, rng.range(0, Math.PI * 2));
1221
+ let lightBudget = options.lampLights ?? 3;
1222
+ for (let i = 0; i < houseCount; i++) {
1223
+ const angle = i / houseCount * Math.PI * 2 + rng.range(-0.15, 0.15);
1224
+ const house = createHouse({ seed: rng.int(1, 1e9), palette });
1225
+ const spot = findSpot(angle, radius * rng.range(0.7, 0.95), house.obstacleRadius);
1226
+ const facing = Math.atan2(center.x - spot.x, center.z - spot.z);
1227
+ place(house, spot.x, spot.z, facing);
1228
+ lamps.push(house);
1229
+ if (rng.next() < 0.7) {
1230
+ const crateAngle = angle + rng.range(-0.5, 0.5);
1231
+ const crateDistance = Math.hypot(spot.x - center.x, spot.z - center.z) - house.obstacleRadius - 0.7;
1232
+ place(
1233
+ createCrate({ seed: rng.int(1, 1e9), size: rng.range(0.7, 1), palette }),
1234
+ center.x + Math.cos(crateAngle) * crateDistance,
1235
+ center.z + Math.sin(crateAngle) * crateDistance,
1236
+ rng.range(0, Math.PI)
1237
+ );
1238
+ }
1239
+ if (i % 2 === 0) {
1240
+ const lampDistance = Math.hypot(spot.x - center.x, spot.z - center.z) * 0.55;
1241
+ const lamp = createLamp({ seed: rng.int(1, 1e9), light: lightBudget > 0, palette });
1242
+ lightBudget--;
1243
+ place(lamp, center.x + Math.cos(angle) * lampDistance, center.z + Math.sin(angle) * lampDistance, 0);
1244
+ lamps.push(lamp);
1245
+ }
1246
+ }
1247
+ if (options.tower ?? true) {
1248
+ const angle = rng.range(0, Math.PI * 2);
1249
+ const tower = createTower({ seed: rng.int(1, 1e9), palette });
1250
+ const spot = findSpot(angle, radius * 1.05, tower.obstacleRadius);
1251
+ place(tower, spot.x, spot.z, rng.range(0, Math.PI * 2));
1252
+ }
1253
+ if (options.ruin ?? true) {
1254
+ const angle = rng.range(0, Math.PI * 2);
1255
+ const ruin = createRuin({ seed: rng.int(1, 1e9), palette });
1256
+ const spot = findSpot(angle, radius * 1.35, ruin.obstacleRadius);
1257
+ place(ruin, spot.x, spot.z, rng.range(0, Math.PI * 2));
1258
+ }
1259
+ return {
1260
+ group,
1261
+ props,
1262
+ obstacles: collectObstacles(props),
1263
+ lamps,
1264
+ keepOut: [{ center: { ...center }, radius: radius * 1.5 }],
1265
+ center
1266
+ };
1267
+ }
1268
+
1269
+ // src/kits/kit.ts
1270
+ var import_three17 = require("three");
1271
+ var KIT_UNIT = 2;
1272
+ function assembleKit(rows, options = {}) {
1273
+ const rng = new Rng(options.seed ?? 1);
1274
+ const palette = options.palette ?? DEFAULT_PALETTE;
1275
+ const unit = options.unit ?? KIT_UNIT;
1276
+ const wallHeight = options.wallHeight ?? 2.6;
1277
+ let torchLights = options.torchLights ?? 4;
1278
+ const height = rows.length;
1279
+ const width = Math.max(...rows.map((r) => r.length), 0);
1280
+ const originX = (-width / 2 + 0.5) * unit;
1281
+ const originZ = (-height / 2 + 0.5) * unit;
1282
+ const worldOf = (col, row) => ({
1283
+ x: originX + col * unit,
1284
+ z: originZ + row * unit
1285
+ });
1286
+ const group = new import_three17.Group();
1287
+ group.name = "kit";
1288
+ const obstacles = [];
1289
+ const spawns = [];
1290
+ const torches = [];
1291
+ const floorCells = /* @__PURE__ */ new Set();
1292
+ const wallCells = [];
1293
+ const floorTiles = [];
1294
+ for (let row = 0; row < height; row++) {
1295
+ for (let col = 0; col < (rows[row] ?? "").length; col++) {
1296
+ const cell = rows[row][col];
1297
+ if (cell === " " || cell === void 0) continue;
1298
+ const { x, z } = worldOf(col, row);
1299
+ if (cell === "#") {
1300
+ wallCells.push({ x, z });
1301
+ obstacles.push({ center: new import_three17.Vector3(x, wallHeight / 2, z), radius: unit * 0.71 });
1302
+ continue;
1303
+ }
1304
+ floorTiles.push({ x, z });
1305
+ floorCells.add(`${col},${row}`);
1306
+ if (cell === "D") {
1307
+ const horizontalRun = rows[row][col - 1] === "#" || rows[row][col + 1] === "#";
1308
+ const lintel = new import_three17.Mesh(
1309
+ new import_three17.BoxGeometry(
1310
+ horizontalRun ? unit : unit * 0.4,
1311
+ wallHeight * 0.25,
1312
+ horizontalRun ? unit * 0.4 : unit
1313
+ ),
1314
+ new import_three17.MeshStandardMaterial({ color: palette.woodDark, flatShading: true })
1315
+ );
1316
+ lintel.position.set(x, wallHeight * 0.875, z);
1317
+ group.add(lintel);
1318
+ } else if (cell === "T") {
1319
+ group.add(createTorch(x, z, palette, rng, torchLights > 0));
1320
+ torchLights--;
1321
+ torches.push(new import_three17.Vector3(x, 0, z));
1322
+ obstacles.push({ center: new import_three17.Vector3(x, 0, z), radius: 0.3 });
1323
+ } else if (cell === "S") {
1324
+ spawns.push(new import_three17.Vector3(x, 0, z));
1325
+ }
1326
+ }
1327
+ }
1328
+ const stone = new import_three17.MeshStandardMaterial({ color: rng.pick(palette.rock), flatShading: true });
1329
+ const stoneDark = new import_three17.MeshStandardMaterial({ color: palette.cliff, flatShading: true });
1330
+ const matrix = new import_three17.Matrix4();
1331
+ if (wallCells.length > 0) {
1332
+ const walls = new import_three17.InstancedMesh(
1333
+ new import_three17.BoxGeometry(unit, wallHeight, unit),
1334
+ stone,
1335
+ wallCells.length
1336
+ );
1337
+ wallCells.forEach((cell, i) => {
1338
+ walls.setMatrixAt(i, matrix.makeTranslation(cell.x, wallHeight / 2, cell.z));
1339
+ });
1340
+ walls.instanceMatrix.needsUpdate = true;
1341
+ group.add(walls);
1342
+ }
1343
+ if (floorTiles.length > 0) {
1344
+ const floors = new import_three17.InstancedMesh(
1345
+ new import_three17.BoxGeometry(unit, 0.2, unit),
1346
+ stoneDark,
1347
+ floorTiles.length
1348
+ );
1349
+ floorTiles.forEach((cell, i) => {
1350
+ floors.setMatrixAt(i, matrix.makeTranslation(cell.x, -0.1, cell.z));
1351
+ });
1352
+ floors.instanceMatrix.needsUpdate = true;
1353
+ group.add(floors);
1354
+ }
1355
+ return {
1356
+ group,
1357
+ obstacles,
1358
+ spawns,
1359
+ torches,
1360
+ floorAt(x, z) {
1361
+ const col = Math.round((x - originX) / unit);
1362
+ const row = Math.round((z - originZ) / unit);
1363
+ return floorCells.has(`${col},${row}`);
1364
+ },
1365
+ size: { width: width * unit, depth: height * unit }
1366
+ };
1367
+ }
1368
+ function createTorch(x, z, palette, rng, light) {
1369
+ const torch = new import_three17.Group();
1370
+ torch.name = "torch";
1371
+ const pole = new import_three17.Mesh(
1372
+ new import_three17.CylinderGeometry(0.04, 0.06, 1.5, 5),
1373
+ new import_three17.MeshStandardMaterial({ color: palette.woodDark, flatShading: true })
1374
+ );
1375
+ pole.position.y = 0.75;
1376
+ torch.add(pole);
1377
+ const flame = new import_three17.Mesh(
1378
+ new import_three17.SphereGeometry(0.1, 6, 5),
1379
+ new import_three17.MeshStandardMaterial({
1380
+ color: palette.lampGlow,
1381
+ emissive: palette.lampGlow,
1382
+ emissiveIntensity: 1.8
1383
+ })
1384
+ );
1385
+ flame.position.y = 1.58;
1386
+ flame.scale.y = rng.range(1.2, 1.5);
1387
+ torch.add(flame);
1388
+ if (light) {
1389
+ const point = new import_three17.PointLight(palette.lampGlow, 5, 9, 1.9);
1390
+ point.position.y = 1.6;
1391
+ torch.add(point);
1392
+ }
1393
+ torch.position.set(x, 0, z);
1394
+ return torch;
1395
+ }
1396
+
1397
+ // src/scene/manifest.ts
1398
+ var import_three19 = require("three");
1399
+
528
1400
  // src/scatter/scatter.ts
529
- var import_three10 = require("three");
1401
+ var import_three18 = require("three");
530
1402
  function scatter(options) {
531
1403
  const seed = options.seed ?? 1;
532
1404
  const rng = new Rng(seed);
@@ -581,7 +1453,7 @@ function scatter(options) {
581
1453
  const itemIndex = pickItem();
582
1454
  const [minScale, maxScale2] = options.items[itemIndex].scale ?? [0.8, 1.25];
583
1455
  placements.push({
584
- position: new import_three10.Vector3(x, y, z),
1456
+ position: new import_three18.Vector3(x, y, z),
585
1457
  rotationY: rng.range(0, Math.PI * 2),
586
1458
  scale: rng.range(minScale, maxScale2),
587
1459
  itemIndex
@@ -591,18 +1463,59 @@ function scatter(options) {
591
1463
  if (!bucket) occupied.set(key, bucket = []);
592
1464
  bucket.push({ x, z });
593
1465
  }
594
- const group = new import_three10.Group();
1466
+ const group = new import_three18.Group();
595
1467
  group.name = "scatter";
596
1468
  const obstacles = [];
597
- const placementMatrix = new import_three10.Matrix4();
598
- const composed = new import_three10.Matrix4();
599
- const quaternion = new import_three10.Quaternion();
600
- const up = new import_three10.Vector3(0, 1, 0);
601
- const scaleVector = new import_three10.Vector3();
1469
+ const placementMatrix = new import_three18.Matrix4();
1470
+ const composed = new import_three18.Matrix4();
1471
+ const quaternion = new import_three18.Quaternion();
1472
+ const up = new import_three18.Vector3(0, 1, 0);
1473
+ const scaleVector = new import_three18.Vector3();
1474
+ const emit = (variant, list, parent) => {
1475
+ variant.object.updateMatrixWorld(true);
1476
+ variant.object.traverse((child) => {
1477
+ if (!(child instanceof import_three18.Mesh)) return;
1478
+ const instanced = new import_three18.InstancedMesh(child.geometry, child.material, list.length);
1479
+ instanced.instanceMatrix.setUsage(import_three18.DynamicDrawUsage);
1480
+ list.forEach((placement, index) => {
1481
+ quaternion.setFromAxisAngle(up, placement.rotationY);
1482
+ scaleVector.setScalar(placement.scale);
1483
+ placementMatrix.compose(placement.position, quaternion, scaleVector);
1484
+ composed.multiplyMatrices(placementMatrix, child.matrixWorld);
1485
+ instanced.setMatrixAt(index, composed);
1486
+ });
1487
+ instanced.instanceMatrix.needsUpdate = true;
1488
+ parent.add(instanced);
1489
+ });
1490
+ };
1491
+ const tileSize = options.lod?.tileSize ?? 16;
1492
+ const tileMap = /* @__PURE__ */ new Map();
1493
+ const tileFor = (x, z) => {
1494
+ const tx = Math.floor(x / tileSize);
1495
+ const tz = Math.floor(z / tileSize);
1496
+ const key = `${tx},${tz}`;
1497
+ let tile = tileMap.get(key);
1498
+ if (!tile) {
1499
+ tile = {
1500
+ center: { x: (tx + 0.5) * tileSize, z: (tz + 0.5) * tileSize },
1501
+ near: new import_three18.Group(),
1502
+ far: new import_three18.Group()
1503
+ };
1504
+ tile.far.visible = false;
1505
+ group.add(tile.near, tile.far);
1506
+ tileMap.set(key, tile);
1507
+ }
1508
+ return tile;
1509
+ };
602
1510
  options.items.forEach((item, itemIndex) => {
603
1511
  const variantCount = item.variants ?? 4;
604
1512
  const variants = [];
605
1513
  for (let v = 0; v < variantCount; v++) variants.push(item.create(rng.fork()));
1514
+ const useLod = options.lod !== void 0 && item.createFar !== void 0;
1515
+ const farVariants = [];
1516
+ if (useLod) {
1517
+ for (let v = 0; v < variantCount; v++) farVariants.push(item.createFar(rng.fork()));
1518
+ }
606
1519
  const byVariant = variants.map(() => []);
607
1520
  for (const placement of placements) {
608
1521
  if (placement.itemIndex !== itemIndex) continue;
@@ -612,21 +1525,21 @@ function scatter(options) {
612
1525
  variants.forEach((variant, v) => {
613
1526
  const list = byVariant[v];
614
1527
  if (list.length === 0) return;
615
- variant.object.updateMatrixWorld(true);
616
- variant.object.traverse((child) => {
617
- if (!(child instanceof import_three10.Mesh)) return;
618
- const instanced = new import_three10.InstancedMesh(child.geometry, child.material, list.length);
619
- instanced.instanceMatrix.setUsage(import_three10.DynamicDrawUsage);
620
- list.forEach((placement, index) => {
621
- quaternion.setFromAxisAngle(up, placement.rotationY);
622
- scaleVector.setScalar(placement.scale);
623
- placementMatrix.compose(placement.position, quaternion, scaleVector);
624
- composed.multiplyMatrices(placementMatrix, child.matrixWorld);
625
- instanced.setMatrixAt(index, composed);
626
- });
627
- instanced.instanceMatrix.needsUpdate = true;
628
- group.add(instanced);
629
- });
1528
+ if (useLod) {
1529
+ const byTile = /* @__PURE__ */ new Map();
1530
+ for (const placement of list) {
1531
+ const tile = tileFor(placement.position.x, placement.position.z);
1532
+ let bucket = byTile.get(tile);
1533
+ if (!bucket) byTile.set(tile, bucket = []);
1534
+ bucket.push(placement);
1535
+ }
1536
+ for (const [tile, bucket] of byTile) {
1537
+ emit(variant, bucket, tile.near);
1538
+ emit(farVariants[v], bucket, tile.far);
1539
+ }
1540
+ } else {
1541
+ emit(variant, list, group);
1542
+ }
630
1543
  if (variant.obstacleRadius > 0) {
631
1544
  for (const placement of list) {
632
1545
  obstacles.push({
@@ -637,23 +1550,263 @@ function scatter(options) {
637
1550
  }
638
1551
  });
639
1552
  });
640
- return { group, placements, obstacles, count: placements.length };
1553
+ const result = { group, placements, obstacles, count: placements.length };
1554
+ if (options.lod) {
1555
+ const tiles = [...tileMap.values()];
1556
+ const swapOut = options.lod.distance * 1.1;
1557
+ const swapIn = options.lod.distance * 0.9;
1558
+ result.tiles = tiles;
1559
+ result.update = (camera) => {
1560
+ for (const tile of tiles) {
1561
+ const dx = camera.position.x - tile.center.x;
1562
+ const dz = camera.position.z - tile.center.z;
1563
+ const distance = Math.hypot(dx, dz);
1564
+ if (tile.near.visible && distance > swapOut) {
1565
+ tile.near.visible = false;
1566
+ tile.far.visible = true;
1567
+ } else if (!tile.near.visible && distance < swapIn) {
1568
+ tile.near.visible = true;
1569
+ tile.far.visible = false;
1570
+ }
1571
+ }
1572
+ };
1573
+ }
1574
+ return result;
1575
+ }
1576
+
1577
+ // src/scene/manifest.ts
1578
+ var PROP_FACTORIES = {
1579
+ tree: (seed, palette) => createTree({ seed, palette }),
1580
+ rock: (seed, palette) => createRock({ seed, palette }),
1581
+ bush: (seed, palette) => createBush({ seed, palette }),
1582
+ grass: (seed, palette) => createGrassTuft({ seed, palette }),
1583
+ crate: (seed, palette) => createCrate({ seed, palette }),
1584
+ fence: (seed, palette) => createFence({ seed, palette }),
1585
+ lamp: (seed, palette) => createLamp({ seed, palette })
1586
+ };
1587
+ var FAR_FACTORIES = {
1588
+ tree: (seed, palette) => {
1589
+ const rng = new Rng(seed);
1590
+ const group = new import_three19.Group();
1591
+ const cone = new import_three19.Mesh(
1592
+ new import_three19.CylinderGeometry(0, rng.range(1, 1.4), rng.range(2.6, 3.8), 5),
1593
+ new import_three19.MeshStandardMaterial({ color: rng.pick(palette.foliage), flatShading: true })
1594
+ );
1595
+ cone.position.y = cone.geometry.parameters.height / 2 + 0.3;
1596
+ group.add(cone);
1597
+ return { object: group, obstacleRadius: 0 };
1598
+ },
1599
+ bush: (seed, palette) => {
1600
+ const rng = new Rng(seed);
1601
+ const group = new import_three19.Group();
1602
+ const cone = new import_three19.Mesh(
1603
+ new import_three19.CylinderGeometry(0.1, rng.range(0.5, 0.7), rng.range(0.5, 0.8), 5),
1604
+ new import_three19.MeshStandardMaterial({ color: rng.pick(palette.foliage), flatShading: true })
1605
+ );
1606
+ cone.position.y = 0.3;
1607
+ group.add(cone);
1608
+ return { object: group, obstacleRadius: 0 };
1609
+ },
1610
+ grass: () => ({ object: new import_three19.Group(), obstacleRadius: 0 })
1611
+ };
1612
+ function buildScene(manifest, scene) {
1613
+ const seed = manifest.seed ?? 1;
1614
+ const palette = manifest.palette ? PALETTES[manifest.palette] : DEFAULT_PALETTE;
1615
+ const group = new import_three19.Group();
1616
+ group.name = "scena-scene";
1617
+ const updates = [];
1618
+ let terrain;
1619
+ if (manifest.terrain) {
1620
+ terrain = createTerrain({
1621
+ ...manifest.terrain,
1622
+ seed,
1623
+ waterLevel: manifest.water?.level,
1624
+ palette
1625
+ });
1626
+ group.add(terrain.mesh);
1627
+ }
1628
+ const heightAt = terrain ? terrain.heightAt : () => 0;
1629
+ let water;
1630
+ if (manifest.water) {
1631
+ water = createWater({
1632
+ level: manifest.water.level,
1633
+ size: manifest.water.size ?? (terrain ? terrain.size * 1.4 : 200),
1634
+ palette
1635
+ });
1636
+ group.add(water.mesh);
1637
+ updates.push((dt) => water.update(dt));
1638
+ }
1639
+ const dryLand = terrain && water ? aboveWater(terrain, water, 0.3) : () => true;
1640
+ let sky;
1641
+ if (manifest.sky ?? true) {
1642
+ sky = createSky({ palette });
1643
+ group.add(sky.mesh);
1644
+ }
1645
+ const rig = createLightingRig(manifest.lighting ?? "day");
1646
+ group.add(rig.group);
1647
+ if (scene && manifest.fog !== false) applyFog(scene, manifest.fog ?? "haze", palette);
1648
+ const paths = (manifest.paths ?? []).map((spec) => {
1649
+ const path = createPath(spec.points, {
1650
+ surface: heightAt,
1651
+ width: spec.width,
1652
+ loop: spec.loop,
1653
+ palette
1654
+ });
1655
+ group.add(path.mesh);
1656
+ return path;
1657
+ });
1658
+ const onAnyPath = (x, z) => paths.some((p) => p.contains(x, z));
1659
+ let village;
1660
+ if (manifest.village) {
1661
+ village = createVillage({
1662
+ ...manifest.village,
1663
+ seed: seed + 1,
1664
+ surface: heightAt,
1665
+ mask: (x, z) => dryLand(x, z) && !onAnyPath(x, z),
1666
+ palette
1667
+ });
1668
+ group.add(village.group);
1669
+ }
1670
+ const wind = manifest.wind ?? true;
1671
+ const scatters = (manifest.scatters ?? []).map((spec, index) => {
1672
+ const inset = terrain ? terrain.size * 0.45 : 40;
1673
+ const avoidWater = spec.avoidWater ?? true;
1674
+ const avoidPaths = spec.avoidPaths ?? true;
1675
+ const result = scatter({
1676
+ seed: seed + 10 + index,
1677
+ area: spec.area ?? { min: { x: -inset, z: -inset }, max: { x: inset, z: inset } },
1678
+ surface: heightAt,
1679
+ density: spec.density,
1680
+ count: spec.count,
1681
+ minSpacing: spec.minSpacing,
1682
+ clumpScale: spec.clumpScale,
1683
+ lod: spec.lod,
1684
+ items: spec.items.map((item) => {
1685
+ const far = spec.lod ? FAR_FACTORIES[item.type] : void 0;
1686
+ return {
1687
+ create: (rng) => PROP_FACTORIES[item.type](rng.int(1, 1e9), palette),
1688
+ createFar: far && ((rng) => far(rng.int(1, 1e9), palette)),
1689
+ weight: item.weight,
1690
+ variants: item.variants,
1691
+ scale: item.scale
1692
+ };
1693
+ }),
1694
+ mask: (x, z, y) => (spec.maxHeight === void 0 || y < spec.maxHeight) && (!avoidWater || dryLand(x, z)) && (!avoidPaths || !onAnyPath(x, z)),
1695
+ keepOut: [
1696
+ ...avoidPaths ? paths.flatMap((p) => p.keepOut) : [],
1697
+ ...village ? village.keepOut : []
1698
+ ]
1699
+ });
1700
+ group.add(result.group);
1701
+ if (wind !== false) {
1702
+ const sway = applyWind(result.group, {
1703
+ strength: typeof wind === "object" ? wind.strength ?? 0.05 : 0.05
1704
+ });
1705
+ updates.push((dt) => sway.update(dt));
1706
+ }
1707
+ return result;
1708
+ });
1709
+ let cycle;
1710
+ if (manifest.dayCycle) {
1711
+ cycle = createDayCycle({
1712
+ sky,
1713
+ rig,
1714
+ scene,
1715
+ lamps: village?.lamps,
1716
+ palette,
1717
+ dayLength: manifest.dayCycle.dayLength,
1718
+ timeOfDay: manifest.dayCycle.timeOfDay
1719
+ });
1720
+ updates.push((dt) => cycle.update(dt));
1721
+ }
1722
+ scene?.add(group);
1723
+ return {
1724
+ group,
1725
+ palette,
1726
+ terrain,
1727
+ water,
1728
+ sky,
1729
+ rig,
1730
+ cycle,
1731
+ paths,
1732
+ village,
1733
+ scatters,
1734
+ obstacles: [
1735
+ ...village ? village.obstacles : [],
1736
+ ...scatters.flatMap((s) => s.obstacles)
1737
+ ],
1738
+ heightAt,
1739
+ update(dt) {
1740
+ for (const fn of updates) fn(dt);
1741
+ }
1742
+ };
1743
+ }
1744
+
1745
+ // src/scene/markers.ts
1746
+ var import_three20 = require("three");
1747
+ function extractMarkers(root) {
1748
+ root.updateWorldMatrix(true, true);
1749
+ const spawns = {};
1750
+ const routePoints = {};
1751
+ const obstacles = [];
1752
+ const keepOut = [];
1753
+ root.traverse((node) => {
1754
+ const name = node.name.replace(/\.\d+$/, "");
1755
+ const match = /^(spawn|route|obstacle|keepout)_(.+)$/.exec(name);
1756
+ if (!match) return;
1757
+ const [, kind, rest] = match;
1758
+ const position = node.getWorldPosition(new import_three20.Vector3());
1759
+ const radius = Math.max(node.scale.x, node.scale.z);
1760
+ if (kind === "spawn") {
1761
+ spawns[rest] = position;
1762
+ } else if (kind === "route") {
1763
+ const step = /^(.*)_(\d+)$/.exec(rest);
1764
+ const routeName = step ? step[1] : rest;
1765
+ const index = step ? parseInt(step[2], 10) : 0;
1766
+ (routePoints[routeName] ?? (routePoints[routeName] = [])).push({ index, position });
1767
+ } else if (kind === "obstacle") {
1768
+ obstacles.push({ center: position, radius });
1769
+ } else {
1770
+ keepOut.push({ center: { x: position.x, z: position.z }, radius });
1771
+ }
1772
+ });
1773
+ const routes = {};
1774
+ for (const [name, points] of Object.entries(routePoints)) {
1775
+ routes[name] = points.sort((a, b) => a.index - b.index).map((p) => p.position);
1776
+ }
1777
+ return { spawns, routes, obstacles, keepOut };
641
1778
  }
642
1779
  // Annotate the CommonJS export names for ESM import in node:
643
1780
  0 && (module.exports = {
644
1781
  DEFAULT_PALETTE,
1782
+ KIT_UNIT,
645
1783
  PALETTES,
646
1784
  Rng,
1785
+ aboveWater,
647
1786
  applyFog,
1787
+ applyWind,
1788
+ assembleKit,
1789
+ buildScene,
648
1790
  collectObstacles,
1791
+ createBush,
649
1792
  createCrate,
1793
+ createDayCycle,
650
1794
  createFence,
1795
+ createGrassTuft,
1796
+ createHouse,
651
1797
  createLamp,
652
1798
  createLightingRig,
1799
+ createPath,
653
1800
  createRock,
1801
+ createRuin,
654
1802
  createSky,
655
1803
  createTerrain,
1804
+ createTower,
656
1805
  createTree,
1806
+ createVillage,
1807
+ createWater,
1808
+ createWell,
1809
+ extractMarkers,
657
1810
  fractalNoise2,
658
1811
  hash2,
659
1812
  scatter,