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