minerender 2.1.0-alpha.114 → 2.1.0-alpha.116

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.
@@ -323,6 +323,9 @@ var init_Caching = __esm({
323
323
  static {
324
324
  this.usernameUuidCache = Caches.builder().expireAfterWrite(Time.minutes(5)).expirationInterval(Time.seconds(30)).buildAsync();
325
325
  }
326
+ static {
327
+ this.biomeCache = _Caching.createAssetCache();
328
+ }
326
329
  static createAssetCache() {
327
330
  return Caches.builder().expireAfterWrite(Time.minutes(10)).expireAfterAccess(Time.minutes(5)).expirationInterval(Time.seconds(30)).buildAsync();
328
331
  }
@@ -352,7 +355,8 @@ var init_Caching = __esm({
352
355
  this.fontCache,
353
356
  this.bannerPatternCache,
354
357
  this.mineRenderDataCache,
355
- this.usernameUuidCache
358
+ this.usernameUuidCache,
359
+ this.biomeCache
356
360
  ];
357
361
  }
358
362
  /** Invalidates in-memory entries so later lookups reload them. Persisted entries are retained. */
@@ -1730,7 +1734,7 @@ ${key}` : key, async () => {
1730
1734
  case "blockStates":
1731
1735
  return Object.values(value).every((property) => object(property) && "default" in property && Array.isArray(property.values));
1732
1736
  case "blockTints":
1733
- return ["constant", "grass", "stem", "redstone"].includes(value.source) && (value.source !== "constant" || Number.isInteger(value.color));
1737
+ return ["constant", "grass", "stem", "redstone"].includes(value.source) && (value.source !== "constant" || Number.isInteger(value.color)) && (value.biome === void 0 || ["grass", "foliage", "dry_foliage", "water"].includes(value.biome));
1734
1738
  case "fluids":
1735
1739
  return ["water", "lava"].includes(value.kind) && typeof value.renderModel === "boolean" && (value.levelProperty === void 0 || typeof value.levelProperty === "string");
1736
1740
  case "dyes":
@@ -1874,15 +1878,19 @@ var init_Colormaps = __esm({
1874
1878
  Colormaps = class {
1875
1879
  /** Samples the selected resource pack's grass colormap using Minecraft's climate coordinates. */
1876
1880
  static async grassColor(origin, temperature = 0.5, downfall = 1) {
1877
- const key = AssetKey.parse("textures", "minecraft:colormap/grass", origin);
1881
+ return this.getColor("grass", origin, temperature, downfall);
1882
+ }
1883
+ /** Samples a resource-pack colormap at the supplied climate coordinates. */
1884
+ static async getColor(kind, origin, temperature = 0.5, downfall = 1) {
1885
+ const key = AssetKey.parse("textures", `minecraft:colormap/${kind}`, origin);
1878
1886
  const temp = Math.fround(temperature);
1879
1887
  const rain = Math.fround(downfall) * temp;
1880
1888
  const x = Math.floor((1 - temp) * 255), y = Math.floor((1 - rain) * 255);
1881
1889
  return await Caching.blockTintCache.get(`${key.serialize()}|${x},${y}`, async () => {
1882
1890
  const image = await ModelTextures.get(key);
1883
- if (!image) throw new MineRenderError(`Missing grass colormap: ${key.toNamespacedString()}`);
1891
+ if (!image) throw new MineRenderError(`Missing ${kind} colormap: ${key.toNamespacedString()}`);
1884
1892
  if (image.width !== 256 || image.height !== 256) {
1885
- throw new MineRenderError("Grass colormaps must be 256 \xD7 256 pixels");
1893
+ throw new MineRenderError(`${kind} colormaps must be 256 \xD7 256 pixels`);
1886
1894
  }
1887
1895
  const [red, green, blue] = image.data.getImageData(x, y, 1, 1).data;
1888
1896
  return red << 16 | green << 8 | blue;
@@ -7751,6 +7759,116 @@ init_AssetLoader();
7751
7759
  init_Assets();
7752
7760
  init_BannerPatterns();
7753
7761
 
7762
+ // src/assets/Biomes.ts
7763
+ init_AssetKey();
7764
+ init_AssetLoader();
7765
+ init_AssetParsers();
7766
+ init_Caching();
7767
+ init_MineRenderError();
7768
+ init_Colormaps();
7769
+
7770
+ // src/assets/_biomes/SwampGrassColor.ts
7771
+ var permutation;
7772
+ var gradients = [[1, 1], [-1, 1], [1, -1], [-1, -1], [1, 0], [-1, 0], [1, 0], [-1, 0], [0, 1], [0, -1], [0, 1], [0, -1]];
7773
+ var skew = (Math.sqrt(3) - 1) / 2;
7774
+ var unskew = (3 - Math.sqrt(3)) / 6;
7775
+ function createPermutation() {
7776
+ const multiplier = 0x5deece66dn, mask = (1n << 48n) - 1n;
7777
+ let seed = 2345n ^ multiplier;
7778
+ const next = (bits) => {
7779
+ seed = seed * multiplier + 11n & mask;
7780
+ return Number(seed >> BigInt(48 - bits));
7781
+ };
7782
+ for (let i = 0; i < 3; i++) {
7783
+ next(26);
7784
+ next(27);
7785
+ }
7786
+ const values = Uint8Array.from({ length: 256 }, (_, i) => i);
7787
+ for (let i = 0; i < 256; i++) {
7788
+ const bound = 256 - i;
7789
+ let offset;
7790
+ if ((bound & bound - 1) === 0) offset = Math.floor(bound * next(31) / 2147483648);
7791
+ else {
7792
+ let bits;
7793
+ do {
7794
+ bits = next(31);
7795
+ offset = bits % bound;
7796
+ } while (bits - offset + bound - 1 > 2147483647);
7797
+ }
7798
+ const other = i + offset;
7799
+ [values[i], values[other]] = [values[other], values[i]];
7800
+ }
7801
+ return values;
7802
+ }
7803
+ function swampGrassColor(x, z) {
7804
+ const p10 = permutation ??= createPermutation();
7805
+ x *= 0.0225;
7806
+ z *= 0.0225;
7807
+ const s = (x + z) * skew;
7808
+ const i = Math.floor(x + s), j = Math.floor(z + s);
7809
+ const t = (i + j) * unskew;
7810
+ const dx = x - (i - t), dz = z - (j - t);
7811
+ const stepX = dx > dz ? 1 : 0, stepZ = 1 - stepX;
7812
+ const corner = (gx, gz, a, b) => {
7813
+ let falloff = 0.5 - a * a - b * b;
7814
+ if (falloff < 0) return 0;
7815
+ const gradient = gradients[p10[gx + p10[gz & 255] & 255] % 12];
7816
+ falloff *= falloff;
7817
+ return falloff * falloff * (gradient[0] * a + gradient[1] * b);
7818
+ };
7819
+ const noise = 70 * (corner(i, j, dx, dz) + corner(i + stepX, j + stepZ, dx - stepX + unskew, dz - stepZ + unskew) + corner(i + 1, j + 1, dx - 1 + 2 * unskew, dz - 1 + 2 * unskew));
7820
+ return noise < -0.1 ? 5011004 : 6975545;
7821
+ }
7822
+
7823
+ // src/assets/Biomes.ts
7824
+ var Biomes = class {
7825
+ /** Loads a biome registry entry, or returns `undefined` when no source provides it. */
7826
+ static async get(id, origin) {
7827
+ if (!/^(?:[a-z0-9_.-]+:)?[a-z0-9_./-]+$/.test(id)) throw new MineRenderError(`Invalid biome identifier: ${id}`);
7828
+ const [namespace, path] = id.includes(":") ? id.split(":") : ["minecraft", id];
7829
+ const key = new AssetKey(namespace, path, "worldgen", "biome", "data", ".json", origin?.root);
7830
+ return Caching.biomeCache.get(key.serialize(), async () => {
7831
+ const asset2 = await AssetLoader.get(key, "json" /* JSON */);
7832
+ if (asset2 === void 0) return void 0;
7833
+ if (!asset2 || typeof asset2 !== "object" || Array.isArray(asset2) || !asset2.effects || typeof asset2.effects !== "object" || Array.isArray(asset2.effects)) {
7834
+ throw new MineRenderError(`Invalid biome effects: ${id}`);
7835
+ }
7836
+ if (typeof asset2.temperature !== "number" || !Number.isFinite(asset2.temperature) || typeof asset2.downfall !== "number" || !Number.isFinite(asset2.downfall)) {
7837
+ throw new MineRenderError(`Invalid biome climate: ${id}`);
7838
+ }
7839
+ for (const kind of ["grass", "foliage", "dry_foliage", "water"]) {
7840
+ const color2 = asset2.effects[`${kind}_color`];
7841
+ if (color2 !== void 0) this.parseColor(color2, id, kind);
7842
+ }
7843
+ const modifier = asset2.effects.grass_color_modifier;
7844
+ if (modifier !== void 0 && modifier !== "none" && modifier !== "dark_forest" && modifier !== "swamp") {
7845
+ throw new MineRenderError(`Invalid biome grass color modifier: ${id}`);
7846
+ }
7847
+ return asset2;
7848
+ });
7849
+ }
7850
+ /** Returns packed sRGB, or `undefined` when the biome or requested water color is missing. */
7851
+ static async getColor(id, kind, origin, x = 0, z = 0) {
7852
+ const biome = await this.get(id, origin);
7853
+ if (!biome) return void 0;
7854
+ const rawColor = biome.effects[`${kind}_color`];
7855
+ let color2 = rawColor === void 0 ? void 0 : this.parseColor(rawColor, id, kind);
7856
+ if (kind === "water") return color2;
7857
+ const modifier = kind === "grass" ? biome.effects.grass_color_modifier : void 0;
7858
+ if (modifier === "swamp") return swampGrassColor(x, z);
7859
+ if (color2 === void 0) {
7860
+ const clamp = (value) => Math.max(0, Math.min(1, Math.fround(value)));
7861
+ color2 = await Colormaps.getColor(kind, origin, clamp(biome.temperature), clamp(biome.downfall));
7862
+ }
7863
+ return modifier === "dark_forest" ? (color2 & 16711422) + 2634762 >> 1 : color2;
7864
+ }
7865
+ static parseColor(value, id, kind) {
7866
+ if (typeof value === "string" && /^#[0-9a-fA-F]{6}$/.test(value)) return parseInt(value.slice(1), 16);
7867
+ if (typeof value === "number" && Number.isInteger(value) && value >= 0 && value <= 16777215) return value;
7868
+ throw new MineRenderError(`Invalid biome ${kind} color: ${id}`);
7869
+ }
7870
+ };
7871
+
7754
7872
  // src/assets/BlockEntities.ts
7755
7873
  init_AssetKey();
7756
7874
  init_AssetLoader();
@@ -12230,8 +12348,20 @@ var BlockStateResolver = class _BlockStateResolver {
12230
12348
  init_Colormaps();
12231
12349
  init_MineRenderData();
12232
12350
  var BlockTints = class {
12233
- /** Resolves block preview colors without biome context; explicit tints take precedence. */
12234
- static async get(key, state, model, tints) {
12351
+ /** Returns a block's versioned biome color source, leaving fixed-color blocks unchanged. */
12352
+ static async biomeSource(key) {
12353
+ const rule = key && (await MineRenderData.get("blockTints", key.root))[key.toNamespacedString()];
12354
+ return rule ? rule.biome ?? (rule.source === "grass" ? "grass" : void 0) : void 0;
12355
+ }
12356
+ /** Resolves one block's biome tint at world block coordinates, without biome blending. */
12357
+ static async getBiomeColor(key, biome, position) {
12358
+ if (!biome) return void 0;
12359
+ const x = position?.[0], z = position?.[2];
12360
+ const source = await this.biomeSource(key);
12361
+ return source ? Biomes.getColor(biome, source, key, x, z) : void 0;
12362
+ }
12363
+ /** Resolves used tint indices from biome or preview colors; explicit tints take precedence. */
12364
+ static async get(key, state, model, tints, biomeColor) {
12235
12365
  if (!key) return tints;
12236
12366
  const candidates = (model.elements ?? []).flatMap((element) => Object.values(element.faces)).some((face) => face?.tintindex !== void 0 && face.tintindex >= 0 && tints?.[face.tintindex] === void 0);
12237
12367
  if (!candidates) return tints;
@@ -12247,8 +12377,12 @@ var BlockTints = class {
12247
12377
  }
12248
12378
  }
12249
12379
  if (!indices.size) return tints;
12380
+ const source = rule.biome ?? (rule.source === "grass" ? "grass" : void 0);
12381
+ const biome = source ? typeof biomeColor === "function" ? await biomeColor() : biomeColor : void 0;
12250
12382
  let color2;
12251
- switch (rule.source) {
12383
+ if (biome !== void 0) {
12384
+ color2 = biome;
12385
+ } else switch (rule.source) {
12252
12386
  case "constant":
12253
12387
  color2 = rule.color;
12254
12388
  break;
@@ -12307,6 +12441,7 @@ var BlockObject = class _BlockObject extends SceneObject {
12307
12441
  this._entityGeneration = 0;
12308
12442
  this.options = (0, import_ts_deepmerge6.default)({}, _BlockObject.DEFAULT_OPTIONS, options ?? {});
12309
12443
  this.variantPosition = options?.variantPosition ? [...options.variantPosition] : void 0;
12444
+ this.biomePosition = options?.biomePosition ? [...options.biomePosition] : void 0;
12310
12445
  }
12311
12446
  static {
12312
12447
  this.DEFAULT_OPTIONS = (0, import_ts_deepmerge6.default)({}, ModelObject.DEFAULT_OPTIONS, {
@@ -12504,11 +12639,16 @@ var BlockObject = class _BlockObject extends SceneObject {
12504
12639
  const { FluidModelObject: FluidModelObject2, sampleFluid: sampleFluid2 } = await Promise.resolve().then(() => (init_FluidModelObject(), FluidModelObject_exports));
12505
12640
  this._fluidSampler = sample;
12506
12641
  const surface = sampleFluid2(kind, (x, y, z) => x === 0 && y === 0 && z === 0 ? { fluid: kind, level: this.fluidLevel } : sample?.(x, y, z) ?? {});
12507
- if (this._fluidKey === surface.key) return;
12642
+ const tint = this.options.tints?.[0] ?? this.waterTint;
12643
+ const fluidKey = `${surface.key}|${tint ?? "default"}`;
12644
+ if (this._fluidKey === fluidKey) return;
12508
12645
  const key = new AssetKey("minecraft", `${kind}/${surface.key}`, "models", "fluid", "assets", ".json", this.blockState.key?.root);
12509
12646
  const replacement = await this.scene.addSceneObject(
12510
12647
  { key },
12511
- () => new FluidModelObject2(kind, surface.sample, this.blockState.key, this.options)
12648
+ () => new FluidModelObject2(kind, surface.sample, this.blockState.key, {
12649
+ ...this.options,
12650
+ tints: tint === void 0 ? this.options.tints : { ...this.options.tints, 0: tint }
12651
+ })
12512
12652
  );
12513
12653
  const previous = this._fluidModel;
12514
12654
  const matrix = previous ? this.getModelMatrix(previous) : new Matrix411().makeTranslation(...this.position.toArray());
@@ -12517,7 +12657,7 @@ var BlockObject = class _BlockObject extends SceneObject {
12517
12657
  this._models = this._models.filter((model) => model !== previous);
12518
12658
  this._models.push(replacement);
12519
12659
  this._fluidModel = replacement;
12520
- this._fluidKey = surface.key;
12660
+ this._fluidKey = fluidKey;
12521
12661
  this._isInstanced ||= isInstanceReference(replacement);
12522
12662
  this._instanceCounter = this._isInstanced ? 1 : 0;
12523
12663
  if (previous) this.removeModels([previous]);
@@ -12540,6 +12680,7 @@ var BlockObject = class _BlockObject extends SceneObject {
12540
12680
  }
12541
12681
  async recreateModels() {
12542
12682
  this.clearModels();
12683
+ this.waterTint = this.fluidKind === "water" && this.options.biome && this.options.tints?.[0] === void 0 ? await Biomes.getColor(this.options.biome, "water", this.blockState.key) : void 0;
12543
12684
  if (resolveBlockFluidState(this.blockState.key, this.state, this._fluidRules)?.renderModel === false) {
12544
12685
  await this.updateFluid(this._fluidSampler);
12545
12686
  return;
@@ -12574,7 +12715,13 @@ var BlockObject = class _BlockObject extends SceneObject {
12574
12715
  const model = await Models.getMerged(modelKey);
12575
12716
  const options = {
12576
12717
  ...this.options,
12577
- tints: model ? await BlockTints.get(this.blockState.key, this.state, model, this.options.tints) : this.options.tints,
12718
+ tints: model ? await BlockTints.get(
12719
+ this.blockState.key,
12720
+ this.state,
12721
+ model,
12722
+ this.options.tints,
12723
+ () => BlockTints.getBiomeColor(this.blockState.key, this.options.biome, this.biomePosition)
12724
+ ) : this.options.tints,
12578
12725
  uvLockRotation: variant.uvlock && (rotation.x !== 0 || rotation.y !== 0) ? [rotation.x, rotation.y, rotation.z] : void 0,
12579
12726
  cullMask: this.options.displayPosition ? 0 : ModelCulling.toLocalMask(this._cullMask, rotation)
12580
12727
  };
@@ -13157,6 +13304,321 @@ init_ModelObject();
13157
13304
  // src/index.ts
13158
13305
  init_nbt();
13159
13306
 
13307
+ // src/renderer/FlyControls.ts
13308
+ import { Controls, Euler as Euler11, MathUtils as MathUtils2, Quaternion as Quaternion6, Vector3 as Vector313 } from "three";
13309
+ var VANILLA_FLY_SPEED = 10.89;
13310
+ var PITCH_LIMIT = Math.PI / 2;
13311
+ var MOVE_EPSILON = 1e-6;
13312
+ var _euler = new Euler11(0, 0, 0, "YXZ");
13313
+ var _forward = new Vector313();
13314
+ var _right = new Vector313();
13315
+ var _targetVelocity = new Vector313();
13316
+ var FlyControls = class extends Controls {
13317
+ constructor(camera, domElement) {
13318
+ super(camera, domElement ?? null);
13319
+ this.isFlyControls = true;
13320
+ /** Horizontal and vertical speed in scene units per second; vanilla creative flight by default. */
13321
+ this.movementSpeed = 16 * VANILLA_FLY_SPEED;
13322
+ /** Speed multiplier while the sprint key is held. */
13323
+ this.sprintMultiplier = 2;
13324
+ /** Radians per pixel of pointer movement. */
13325
+ this.lookSpeed = 2e-3;
13326
+ /** Smooth acceleration and deceleration, approximating vanilla flight momentum. */
13327
+ this.enableDamping = true;
13328
+ /** Portion of the remaining velocity change applied per 60 Hz frame (0–1) when damping is enabled. */
13329
+ this.dampingFactor = 0.25;
13330
+ /** Capture the pointer on click so the mouse turns the camera without dragging; dragging works regardless. */
13331
+ this.pointerLock = true;
13332
+ /** Scroll the wheel to change `movementSpeed` within `speedRange`. */
13333
+ this.enableWheelSpeed = true;
13334
+ /** Minimum and maximum `movementSpeed` reachable with the wheel. */
13335
+ this.speedRange = [16, 16 * 500];
13336
+ /** Touch joystick travel in CSS pixels for full speed. */
13337
+ this.joystickRadius = 64;
13338
+ /** Key bindings by `KeyboardEvent.code`. */
13339
+ this.keys = {
13340
+ forward: ["KeyW", "ArrowUp"],
13341
+ back: ["KeyS", "ArrowDown"],
13342
+ left: ["KeyA", "ArrowLeft"],
13343
+ right: ["KeyD", "ArrowRight"],
13344
+ up: ["Space"],
13345
+ down: ["ShiftLeft", "ShiftRight"],
13346
+ sprint: ["ControlLeft", "ControlRight"]
13347
+ };
13348
+ this._pressed = /* @__PURE__ */ new Set();
13349
+ this._velocity = new Vector313();
13350
+ this._lastPosition = new Vector313();
13351
+ this._lastQuaternion = new Quaternion6();
13352
+ this._position0 = new Vector313();
13353
+ this._quaternion0 = new Quaternion6();
13354
+ this._dragMoved = false;
13355
+ this._locked = false;
13356
+ this._onKeyDown = (event) => this.handleKey(event, true);
13357
+ this._onKeyUp = (event) => this.handleKey(event, false);
13358
+ this._onPointerDown = (event) => this.handlePointerDown(event);
13359
+ this._onPointerMove = (event) => this.handlePointerMove(event);
13360
+ this._onPointerUp = (event) => this.handlePointerUp(event);
13361
+ this._onPointerLockChange = () => this.handlePointerLockChange();
13362
+ this._onContextMenu = (event) => event.preventDefault();
13363
+ this._onWheel = (event) => this.handleWheel(event);
13364
+ this._onBlur = () => this.releaseKeys();
13365
+ this._lastPosition.copy(camera.position);
13366
+ this._lastQuaternion.copy(camera.quaternion);
13367
+ this.saveState();
13368
+ if (domElement) this.connect(domElement);
13369
+ }
13370
+ connect(element) {
13371
+ super.connect(element);
13372
+ const document2 = element.ownerDocument;
13373
+ document2.addEventListener("keydown", this._onKeyDown);
13374
+ document2.addEventListener("keyup", this._onKeyUp);
13375
+ document2.addEventListener("pointerlockchange", this._onPointerLockChange);
13376
+ document2.defaultView?.addEventListener("blur", this._onBlur);
13377
+ element.addEventListener("pointerdown", this._onPointerDown);
13378
+ element.addEventListener("pointermove", this._onPointerMove);
13379
+ element.addEventListener("pointerup", this._onPointerUp);
13380
+ element.addEventListener("pointercancel", this._onPointerUp);
13381
+ element.addEventListener("contextmenu", this._onContextMenu);
13382
+ element.addEventListener("wheel", this._onWheel, { passive: false });
13383
+ element.addEventListener("blur", this._onBlur);
13384
+ element.style.touchAction = "none";
13385
+ if (element.tabIndex < 0) element.tabIndex = 0;
13386
+ }
13387
+ disconnect() {
13388
+ const element = this.domElement;
13389
+ if (!element) return;
13390
+ const document2 = element.ownerDocument;
13391
+ document2.removeEventListener("keydown", this._onKeyDown);
13392
+ document2.removeEventListener("keyup", this._onKeyUp);
13393
+ document2.removeEventListener("pointerlockchange", this._onPointerLockChange);
13394
+ document2.defaultView?.removeEventListener("blur", this._onBlur);
13395
+ element.removeEventListener("pointerdown", this._onPointerDown);
13396
+ element.removeEventListener("pointermove", this._onPointerMove);
13397
+ element.removeEventListener("pointerup", this._onPointerUp);
13398
+ element.removeEventListener("pointercancel", this._onPointerUp);
13399
+ element.removeEventListener("contextmenu", this._onContextMenu);
13400
+ element.removeEventListener("wheel", this._onWheel);
13401
+ element.removeEventListener("blur", this._onBlur);
13402
+ element.style.touchAction = "auto";
13403
+ this.unlock();
13404
+ this._locked = false;
13405
+ this.releaseKeys();
13406
+ this._dragPointer = void 0;
13407
+ this._joystick = void 0;
13408
+ }
13409
+ dispose() {
13410
+ this.disconnect();
13411
+ }
13412
+ /** Whether mouse look currently owns the pointer. */
13413
+ get locked() {
13414
+ return this._locked;
13415
+ }
13416
+ /** Requests pointer capture; browsers only allow this from a user gesture. */
13417
+ lock() {
13418
+ const element = this.domElement;
13419
+ if (!element || this._locked || typeof element.requestPointerLock !== "function") return;
13420
+ try {
13421
+ const result = element.requestPointerLock();
13422
+ if (result instanceof Promise) result.catch(() => void 0);
13423
+ } catch {
13424
+ }
13425
+ }
13426
+ /** Releases a captured pointer. */
13427
+ unlock() {
13428
+ const document2 = this.domElement?.ownerDocument;
13429
+ if (document2?.pointerLockElement && document2.pointerLockElement === this.domElement) {
13430
+ document2.exitPointerLock();
13431
+ }
13432
+ }
13433
+ /** Stores the camera pose that {@link reset} restores. */
13434
+ saveState() {
13435
+ this._position0.copy(this.object.position);
13436
+ this._quaternion0.copy(this.object.quaternion);
13437
+ }
13438
+ /** Restores the saved camera pose and stops movement. */
13439
+ reset() {
13440
+ this.object.position.copy(this._position0);
13441
+ this.object.quaternion.copy(this._quaternion0);
13442
+ this._velocity.set(0, 0, 0);
13443
+ this.notifyChange();
13444
+ }
13445
+ /** Horizontal yaw in radians, where zero looks toward -Z. */
13446
+ get yaw() {
13447
+ return _euler.setFromQuaternion(this.object.quaternion).y;
13448
+ }
13449
+ /** Vertical pitch in radians, positive looking up. */
13450
+ get pitch() {
13451
+ return _euler.setFromQuaternion(this.object.quaternion).x;
13452
+ }
13453
+ /** Turns the camera; pitch is clamped to straight up and down. */
13454
+ setRotation(yaw, pitch) {
13455
+ _euler.set(MathUtils2.clamp(pitch, -PITCH_LIMIT, PITCH_LIMIT), yaw, 0);
13456
+ this.object.quaternion.setFromEuler(_euler);
13457
+ this.notifyChange();
13458
+ }
13459
+ /** Points the camera at a position without rolling it. */
13460
+ lookAt(target) {
13461
+ _forward.copy(target).sub(this.object.position);
13462
+ if (_forward.lengthSq() < MOVE_EPSILON) return;
13463
+ _forward.normalize();
13464
+ this.setRotation(Math.atan2(-_forward.x, -_forward.z), Math.asin(MathUtils2.clamp(_forward.y, -1, 1)));
13465
+ }
13466
+ /** Whether an action's key is held. */
13467
+ isPressed(action) {
13468
+ return this._pressed.has(action);
13469
+ }
13470
+ /**
13471
+ * Advances movement by `delta` seconds (measured since the previous call when omitted)
13472
+ * and dispatches `change` when the camera moved.
13473
+ */
13474
+ update(delta) {
13475
+ if (delta === void 0) {
13476
+ const now = typeof performance !== "undefined" ? performance.now() : Date.now();
13477
+ delta = this._lastTime === void 0 ? 0 : (now - this._lastTime) / 1e3;
13478
+ this._lastTime = now;
13479
+ }
13480
+ if (!this.enabled) {
13481
+ this._velocity.set(0, 0, 0);
13482
+ return;
13483
+ }
13484
+ if (delta > 0) this.move(delta);
13485
+ this.notifyChange();
13486
+ }
13487
+ move(delta) {
13488
+ const camera = this.object;
13489
+ let forward = (this._pressed.has("forward") ? 1 : 0) - (this._pressed.has("back") ? 1 : 0);
13490
+ let strafe = (this._pressed.has("right") ? 1 : 0) - (this._pressed.has("left") ? 1 : 0);
13491
+ const climb = (this._pressed.has("up") ? 1 : 0) - (this._pressed.has("down") ? 1 : 0);
13492
+ const joystick = this._joystick;
13493
+ if (joystick) {
13494
+ const radius = Math.max(this.joystickRadius, 1);
13495
+ forward = MathUtils2.clamp(forward + (joystick.originY - joystick.y) / radius, -1, 1);
13496
+ strafe = MathUtils2.clamp(strafe + (joystick.x - joystick.originX) / radius, -1, 1);
13497
+ }
13498
+ _euler.setFromQuaternion(camera.quaternion);
13499
+ const yaw = _euler.y;
13500
+ _forward.set(-Math.sin(yaw), 0, -Math.cos(yaw));
13501
+ _right.set(Math.cos(yaw), 0, -Math.sin(yaw));
13502
+ _targetVelocity.set(0, 0, 0);
13503
+ if (forward || strafe || climb) {
13504
+ _targetVelocity.addScaledVector(_forward, forward).addScaledVector(_right, strafe);
13505
+ if (_targetVelocity.lengthSq() > 1) _targetVelocity.normalize();
13506
+ _targetVelocity.y = climb;
13507
+ const speed = this.movementSpeed * (this._pressed.has("sprint") ? this.sprintMultiplier : 1);
13508
+ _targetVelocity.multiplyScalar(speed);
13509
+ }
13510
+ if (this.enableDamping) {
13511
+ const blend = 1 - Math.pow(1 - MathUtils2.clamp(this.dampingFactor, 0, 1), delta * 60);
13512
+ this._velocity.lerp(_targetVelocity, blend);
13513
+ if (_targetVelocity.lengthSq() === 0 && this._velocity.lengthSq() < MOVE_EPSILON) this._velocity.set(0, 0, 0);
13514
+ } else {
13515
+ this._velocity.copy(_targetVelocity);
13516
+ }
13517
+ camera.position.addScaledVector(this._velocity, delta);
13518
+ }
13519
+ notifyChange() {
13520
+ const camera = this.object;
13521
+ if (this._lastPosition.distanceToSquared(camera.position) > MOVE_EPSILON || 8 * (1 - this._lastQuaternion.dot(camera.quaternion)) > MOVE_EPSILON) {
13522
+ this._lastPosition.copy(camera.position);
13523
+ this._lastQuaternion.copy(camera.quaternion);
13524
+ this.dispatchEvent({ type: "change" });
13525
+ }
13526
+ }
13527
+ releaseKeys() {
13528
+ this._pressed.clear();
13529
+ }
13530
+ handleKey(event, down) {
13531
+ const action = this.actionFor(event.code);
13532
+ if (!action) return;
13533
+ const ownsInput = this.enabled && (this._locked || event.target === this.domElement);
13534
+ if (down && !ownsInput) return;
13535
+ const pressed = this._pressed.has(action);
13536
+ if (down) this._pressed.add(action);
13537
+ else this._pressed.delete(action);
13538
+ if (ownsInput || this.enabled && pressed) event.preventDefault();
13539
+ }
13540
+ actionFor(code) {
13541
+ for (const action in this.keys) {
13542
+ if (this.keys[action].includes(code)) return action;
13543
+ }
13544
+ return void 0;
13545
+ }
13546
+ handlePointerDown(event) {
13547
+ const element = this.domElement;
13548
+ if (!this.enabled || !element) return;
13549
+ element.focus?.({ preventScroll: true });
13550
+ if (this._locked) return;
13551
+ if (event.pointerType === "touch" && !this._joystick && this.onJoystickSide(event)) {
13552
+ this._joystick = { id: event.pointerId, originX: event.clientX, originY: event.clientY, x: event.clientX, y: event.clientY };
13553
+ element.setPointerCapture?.(event.pointerId);
13554
+ return;
13555
+ }
13556
+ if (this._dragPointer !== void 0) return;
13557
+ if (event.pointerType === "mouse" && event.button !== 0 && event.button !== 2) return;
13558
+ this._dragPointer = event.pointerId;
13559
+ this._dragMoved = false;
13560
+ element.setPointerCapture?.(event.pointerId);
13561
+ }
13562
+ onJoystickSide(event) {
13563
+ const rect = this.domElement?.getBoundingClientRect?.();
13564
+ return !!rect && event.clientX < rect.left + rect.width / 2;
13565
+ }
13566
+ handlePointerMove(event) {
13567
+ if (!this.enabled) return;
13568
+ const joystick = this._joystick;
13569
+ if (joystick && event.pointerId === joystick.id) {
13570
+ joystick.x = event.clientX;
13571
+ joystick.y = event.clientY;
13572
+ return;
13573
+ }
13574
+ if (!this._locked && event.pointerId !== this._dragPointer) return;
13575
+ const dx = event.movementX ?? 0;
13576
+ const dy = event.movementY ?? 0;
13577
+ if (!dx && !dy) return;
13578
+ this._dragMoved = true;
13579
+ this.turn(dx, dy);
13580
+ }
13581
+ handlePointerUp(event) {
13582
+ if (this._joystick?.id === event.pointerId) {
13583
+ this._joystick = void 0;
13584
+ this.domElement?.releasePointerCapture?.(event.pointerId);
13585
+ return;
13586
+ }
13587
+ if (event.pointerId !== this._dragPointer) return;
13588
+ this._dragPointer = void 0;
13589
+ this.domElement?.releasePointerCapture?.(event.pointerId);
13590
+ if (this.enabled && this.pointerLock && !this._dragMoved && event.pointerType === "mouse" && event.button === 0) {
13591
+ this.lock();
13592
+ }
13593
+ }
13594
+ handleWheel(event) {
13595
+ if (!this.enabled || !this.enableWheelSpeed) return;
13596
+ event.preventDefault();
13597
+ const steps = event.deltaMode === 0 ? event.deltaY / 100 : event.deltaY;
13598
+ const [min, max] = this.speedRange;
13599
+ this.movementSpeed = MathUtils2.clamp(this.movementSpeed * Math.pow(1.15, -steps), Math.min(min, max), Math.max(min, max));
13600
+ }
13601
+ handlePointerLockChange() {
13602
+ const document2 = this.domElement?.ownerDocument;
13603
+ const locked = !!document2 && document2.pointerLockElement === this.domElement;
13604
+ if (locked === this._locked) return;
13605
+ this._locked = locked;
13606
+ if (!locked) this.releaseKeys();
13607
+ this.dispatchEvent({ type: locked ? "lock" : "unlock" });
13608
+ }
13609
+ turn(dx, dy) {
13610
+ _euler.setFromQuaternion(this.object.quaternion);
13611
+ _euler.y -= dx * this.lookSpeed;
13612
+ _euler.x = MathUtils2.clamp(_euler.x - dy * this.lookSpeed, -PITCH_LIMIT, PITCH_LIMIT);
13613
+ _euler.z = 0;
13614
+ this.object.quaternion.setFromEuler(_euler);
13615
+ this.notifyChange();
13616
+ }
13617
+ };
13618
+ function isFlyControls(obj) {
13619
+ return obj?.isFlyControls === true;
13620
+ }
13621
+
13160
13622
  // src/renderer/MineRenderScene.ts
13161
13623
  init_SceneObject();
13162
13624
  var import_ts_deepmerge8 = __toESM(require_dist());
@@ -14224,7 +14686,7 @@ function isMineRenderScene(obj) {
14224
14686
  }
14225
14687
 
14226
14688
  // src/renderer/Renderer.ts
14227
- import { AxesHelper as AxesHelper2, GridHelper, OrthographicCamera, PCFShadowMap, PerspectiveCamera, SRGBColorSpace as SRGBColorSpace7, Vector3 as Vector314, WebGLRenderer } from "three";
14689
+ import { AxesHelper as AxesHelper2, GridHelper, OrthographicCamera, PCFShadowMap, PerspectiveCamera, SRGBColorSpace as SRGBColorSpace7, Vector3 as Vector315, WebGLRenderer } from "three";
14228
14690
  var import_ts_deepmerge9 = __toESM(require_dist());
14229
14691
  init_util();
14230
14692
  init_Model();
@@ -14236,11 +14698,11 @@ import { EffectComposer, RenderPass } from "postprocessing";
14236
14698
  import {
14237
14699
  EventDispatcher,
14238
14700
  MOUSE,
14239
- Quaternion as Quaternion6,
14701
+ Quaternion as Quaternion7,
14240
14702
  Spherical,
14241
14703
  TOUCH,
14242
14704
  Vector2 as Vector26,
14243
- Vector3 as Vector313
14705
+ Vector3 as Vector314
14244
14706
  } from "three";
14245
14707
  var _changeEvent = { type: "change" };
14246
14708
  var _startEvent = { type: "start" };
@@ -14254,7 +14716,7 @@ var OrbitControls = class extends EventDispatcher {
14254
14716
  this.domElement = domElement;
14255
14717
  this.domElement.style.touchAction = "none";
14256
14718
  this.enabled = true;
14257
- this.target = new Vector313();
14719
+ this.target = new Vector314();
14258
14720
  this.minDistance = 0;
14259
14721
  this.maxDistance = Infinity;
14260
14722
  this.minZoom = 0;
@@ -14306,11 +14768,11 @@ var OrbitControls = class extends EventDispatcher {
14306
14768
  state = STATE.NONE;
14307
14769
  };
14308
14770
  this.update = function() {
14309
- const offset = new Vector313();
14310
- const quat = new Quaternion6().setFromUnitVectors(object4.up, new Vector313(0, 1, 0));
14771
+ const offset = new Vector314();
14772
+ const quat = new Quaternion7().setFromUnitVectors(object4.up, new Vector314(0, 1, 0));
14311
14773
  const quatInverse = quat.clone().invert();
14312
- const lastPosition = new Vector313();
14313
- const lastQuaternion = new Quaternion6();
14774
+ const lastPosition = new Vector314();
14775
+ const lastQuaternion = new Quaternion7();
14314
14776
  const twoPI = 2 * Math.PI;
14315
14777
  return function update() {
14316
14778
  const position = scope.object.position;
@@ -14399,7 +14861,7 @@ var OrbitControls = class extends EventDispatcher {
14399
14861
  const spherical = new Spherical();
14400
14862
  const sphericalDelta = new Spherical();
14401
14863
  let scale = 1;
14402
- const panOffset = new Vector313();
14864
+ const panOffset = new Vector314();
14403
14865
  let zoomChanged = false;
14404
14866
  const rotateStart = new Vector26();
14405
14867
  const rotateEnd = new Vector26();
@@ -14425,7 +14887,7 @@ var OrbitControls = class extends EventDispatcher {
14425
14887
  sphericalDelta.phi -= angle;
14426
14888
  }
14427
14889
  const panLeft = function() {
14428
- const v = new Vector313();
14890
+ const v = new Vector314();
14429
14891
  return function panLeft2(distance, objectMatrix) {
14430
14892
  v.setFromMatrixColumn(objectMatrix, 0);
14431
14893
  v.multiplyScalar(-distance);
@@ -14433,7 +14895,7 @@ var OrbitControls = class extends EventDispatcher {
14433
14895
  };
14434
14896
  }();
14435
14897
  const panUp = function() {
14436
- const v = new Vector313();
14898
+ const v = new Vector314();
14437
14899
  return function panUp2(distance, objectMatrix) {
14438
14900
  if (scope.screenSpacePanning === true) {
14439
14901
  v.setFromMatrixColumn(objectMatrix, 1);
@@ -14446,7 +14908,7 @@ var OrbitControls = class extends EventDispatcher {
14446
14908
  };
14447
14909
  }();
14448
14910
  const pan = function() {
14449
- const offset = new Vector313();
14911
+ const offset = new Vector314();
14450
14912
  return function pan2(deltaX, deltaY) {
14451
14913
  const element = scope.domElement;
14452
14914
  if (scope.object.isPerspectiveCamera) {
@@ -14957,8 +15419,8 @@ var Renderer = class _Renderer {
14957
15419
  top: void 0,
14958
15420
  bottom: void 0
14959
15421
  },
14960
- position: new Vector314(50, 50, 50),
14961
- lookingAt: new Vector314(0, 0, 0)
15422
+ position: new Vector315(50, 50, 50),
15423
+ lookingAt: new Vector315(0, 0, 0)
14962
15424
  },
14963
15425
  render: {
14964
15426
  fpsLimit: 60,
@@ -14973,7 +15435,8 @@ var Renderer = class _Renderer {
14973
15435
  enabled: true
14974
15436
  },
14975
15437
  controls: {
14976
- enabled: false
15438
+ enabled: false,
15439
+ mode: "orbit"
14977
15440
  },
14978
15441
  debug: {
14979
15442
  grid: false,
@@ -15041,20 +15504,77 @@ var Renderer = class _Renderer {
15041
15504
  }
15042
15505
  createControls() {
15043
15506
  if (!this.options.controls?.enabled) return void 0;
15044
- const position = this.camera.position.clone();
15045
- const controls = new OrbitControls(this.camera, this.renderer.domElement);
15046
- this.camera.position.copy(position);
15507
+ return this.createControlsFor(this.options.controls.mode ?? "orbit", this.initialTarget());
15508
+ }
15509
+ initialTarget() {
15047
15510
  const target = this.options.camera.lookingAt;
15048
- if (isVector3(target)) {
15511
+ if (isVector3(target)) return target.clone();
15512
+ if (isTripleArray(target)) return new Vector315(target[0], target[1], target[2]);
15513
+ return new Vector315();
15514
+ }
15515
+ createControlsFor(mode, target) {
15516
+ let controls;
15517
+ if (mode === "fly") {
15518
+ controls = new FlyControls(this.camera, this.renderer.domElement);
15519
+ controls.lookAt(target);
15520
+ controls.saveState();
15521
+ } else {
15522
+ const position = this.camera.position.clone();
15523
+ controls = new OrbitControls(this.camera, this.renderer.domElement);
15524
+ this.camera.position.copy(position);
15049
15525
  controls.target.copy(target);
15050
- } else if (isTripleArray(target)) {
15051
- controls.target.set(target[0], target[1], target[2]);
15526
+ controls.update();
15527
+ controls.saveState();
15052
15528
  }
15053
- controls.update();
15054
- controls.saveState();
15529
+ this._controlsTime = void 0;
15055
15530
  this.registerEventDispatcher(controls);
15056
15531
  return controls;
15057
15532
  }
15533
+ /**
15534
+ * Replaces renderer-owned controls with the selected mode, keeping the camera where it is.
15535
+ * Switching to orbit controls targets the point the camera looks at, at the previous orbit distance.
15536
+ * Returns the new controls, or undefined when controls were disabled at construction or after disposal.
15537
+ */
15538
+ setControlsMode(mode) {
15539
+ const current = this._controls;
15540
+ if (this._disposed || !current) return void 0;
15541
+ if (this.controlsMode === mode) return current;
15542
+ let target;
15543
+ if (isFlyControls(current)) {
15544
+ const distance = this._orbitDistance ?? (this.camera.position.distanceTo(this.initialTarget()) || 1);
15545
+ target = new Vector315(0, 0, -distance).applyQuaternion(this.camera.quaternion).add(this.camera.position);
15546
+ } else {
15547
+ target = current.target.clone();
15548
+ this._orbitDistance = this.camera.position.distanceTo(target);
15549
+ }
15550
+ const enabled = current.enabled;
15551
+ this.disposeControls(current);
15552
+ this._controls = this.createControlsFor(mode, target);
15553
+ this._controls.enabled = enabled;
15554
+ this.dirty = true;
15555
+ return this._controls;
15556
+ }
15557
+ disposeControls(controls) {
15558
+ this._controls = void 0;
15559
+ this.unregisterEventDispatcher(controls);
15560
+ controls.enabled = false;
15561
+ controls.dispose();
15562
+ }
15563
+ /** Advances owned controls; without a frame time, flight movement does not progress. */
15564
+ updateControls(time) {
15565
+ const controls = this._controls;
15566
+ if (!controls?.enabled) {
15567
+ this._controlsTime = void 0;
15568
+ return;
15569
+ }
15570
+ if (!isFlyControls(controls)) {
15571
+ controls.update();
15572
+ return;
15573
+ }
15574
+ const previous = this._controlsTime;
15575
+ if (time !== void 0) this._controlsTime = time;
15576
+ controls.update(time === void 0 || previous === void 0 ? 0 : (time - previous) / 1e3);
15577
+ }
15058
15578
  //</editor-fold>
15059
15579
  init() {
15060
15580
  if (this._disposed) return;
@@ -15131,6 +15651,12 @@ var Renderer = class _Renderer {
15131
15651
  events.add(changeEvent2);
15132
15652
  dispatcher.addEventListener(changeEvent2, this._changeListener);
15133
15653
  }
15654
+ unregisterEventDispatcher(dispatcher) {
15655
+ const events = this._eventDispatchers.get(dispatcher);
15656
+ if (!events) return;
15657
+ for (const event of events) dispatcher.removeEventListener(event, this._changeListener);
15658
+ this._eventDispatchers.delete(dispatcher);
15659
+ }
15134
15660
  /** Updates the camera and canvas size. Width and height are in CSS pixels. */
15135
15661
  resize(width, height) {
15136
15662
  if (this._disposed) return;
@@ -15207,6 +15733,7 @@ var Renderer = class _Renderer {
15207
15733
  this.renderer.setAnimationLoop(null);
15208
15734
  }
15209
15735
  this._nextFrameTime = void 0;
15736
+ this._controlsTime = void 0;
15210
15737
  for (const subscription of this._frameCallbacks.values()) subscription.previous = void 0;
15211
15738
  }
15212
15739
  /**
@@ -15236,10 +15763,7 @@ var Renderer = class _Renderer {
15236
15763
  }
15237
15764
  this._element = void 0;
15238
15765
  if (this._controls) {
15239
- const controls = this._controls;
15240
- this._controls = void 0;
15241
- controls.enabled = false;
15242
- controls.dispose();
15766
+ this.disposeControls(this._controls);
15243
15767
  }
15244
15768
  this.scene.clear();
15245
15769
  for (const helper of this._debugHelpers) {
@@ -15256,9 +15780,7 @@ var Renderer = class _Renderer {
15256
15780
  }
15257
15781
  animate(t = performance.now()) {
15258
15782
  if (!this._running) return;
15259
- if (this._controls?.enabled) {
15260
- this._controls.update();
15261
- }
15783
+ this.updateControls(t);
15262
15784
  if (!this._running) return;
15263
15785
  if (!this.dirty && !this.options.render.renderAlways && !this._frameCallbacks.size && !this._videoExporter) return;
15264
15786
  const interval = this._frameInterval;
@@ -15295,7 +15817,7 @@ var Renderer = class _Renderer {
15295
15817
  /** Draws a fresh frame without starting the animation loop or invoking frame callbacks. */
15296
15818
  renderOnce() {
15297
15819
  if (this._disposed) throw new Error("Cannot render a disposed renderer");
15298
- if (this._controls?.enabled) this._controls.update();
15820
+ this.updateControls();
15299
15821
  this.drawFrame();
15300
15822
  }
15301
15823
  /**
@@ -15325,7 +15847,7 @@ var Renderer = class _Renderer {
15325
15847
  try {
15326
15848
  if (!wasRunning) this.start();
15327
15849
  this._videoExporter = exporter;
15328
- if (this._controls?.enabled) this._controls.update();
15850
+ this.updateControls();
15329
15851
  this.drawFrame();
15330
15852
  } catch (error) {
15331
15853
  exporter.cancel(error);
@@ -15376,6 +15898,19 @@ var Renderer = class _Renderer {
15376
15898
  get controls() {
15377
15899
  return this._controls;
15378
15900
  }
15901
+ /** Renderer-owned OrbitControls, or undefined in fly mode or without controls. */
15902
+ get orbitControls() {
15903
+ return this._controls && !isFlyControls(this._controls) ? this._controls : void 0;
15904
+ }
15905
+ /** Renderer-owned {@link FlyControls}, or undefined in orbit mode or without controls. */
15906
+ get flyControls() {
15907
+ return isFlyControls(this._controls) ? this._controls : void 0;
15908
+ }
15909
+ /** The active renderer-owned controls mode, or undefined without controls. */
15910
+ get controlsMode() {
15911
+ if (!this._controls) return void 0;
15912
+ return isFlyControls(this._controls) ? "fly" : "orbit";
15913
+ }
15379
15914
  ///
15380
15915
  };
15381
15916
 
@@ -16603,14 +17138,14 @@ var AnvilWorldSource = class {
16603
17138
  };
16604
17139
 
16605
17140
  // src/world/Chunk.ts
16606
- import { Vector3 as Vector317 } from "three";
17141
+ import { Color as Color13, Vector3 as Vector318 } from "three";
16607
17142
  init_BlockStates();
16608
17143
  init_AssetKey();
16609
17144
 
16610
17145
  // src/world/MineRenderWorld.ts
16611
17146
  init_Model();
16612
17147
  init_util();
16613
- import { Vector3 as Vector316 } from "three";
17148
+ import { Vector3 as Vector317 } from "three";
16614
17149
  init_AssetKey();
16615
17150
  init_BlockStates();
16616
17151
  init_CubeFace();
@@ -16619,7 +17154,7 @@ init_CubeFace();
16619
17154
  init_AssetKey();
16620
17155
  init_Models();
16621
17156
  init_CubeFace();
16622
- import { BufferAttribute as BufferAttribute7, BufferGeometry as BufferGeometry8, Matrix4 as Matrix412, Vector3 as Vector315 } from "three";
17157
+ import { BufferAttribute as BufferAttribute7, BufferGeometry as BufferGeometry8, Matrix4 as Matrix412, Vector3 as Vector316 } from "three";
16623
17158
  init_ModelCulling();
16624
17159
  init_ModelObject();
16625
17160
  init_UVMapper();
@@ -16673,24 +17208,48 @@ var SectionModels = class {
16673
17208
  if (!Number.isInteger(maxAtlasSize) || maxAtlasSize < 1) throw new RangeError("maxAtlasSize must be a positive integer");
16674
17209
  }
16675
17210
  /** Returns templates for the matched parts, or `undefined` when the block needs an individual render object. */
16676
- async get(blockState3, properties = {}, position) {
16677
- const variantPosition = position ? [...position] : void 0;
16678
- return (await this.prepareState(blockState3, properties))?.pick(variantPosition);
17211
+ async get(blockState3, properties = {}, position, biome) {
17212
+ const biomePosition = position ? [...position] : void 0;
17213
+ const prepared = await this.state(blockState3, properties);
17214
+ if (!prepared) return void 0;
17215
+ return Promise.all(prepared.groups.map(async (group) => {
17216
+ const variant = BlockStateResolver.choose(group, biomePosition);
17217
+ const choice = prepared.choices.get(variant);
17218
+ const tints = await BlockTints.get(
17219
+ blockState3.key,
17220
+ prepared.state,
17221
+ choice.model,
17222
+ void 0,
17223
+ () => BlockTints.getBiomeColor(blockState3.key, biome, biomePosition)
17224
+ );
17225
+ return this.template(variant, choice, tints);
17226
+ }));
17227
+ }
17228
+ /** Resolves section templates for one tint palette, with a synchronous weighted choice for each cell. */
17229
+ async prepareState(blockState3, properties = {}, biomeColor) {
17230
+ const prepared = await this.state(blockState3, properties);
17231
+ if (!prepared) return void 0;
17232
+ let color2;
17233
+ const resolveColor = typeof biomeColor === "function" ? () => color2 ??= biomeColor() : biomeColor;
17234
+ const templates = /* @__PURE__ */ new Map();
17235
+ await Promise.all([...prepared.choices].map(async ([variant, choice]) => {
17236
+ const tints = await BlockTints.get(blockState3.key, prepared.state, choice.model, void 0, resolveColor);
17237
+ templates.set(variant, await this.template(variant, choice, tints));
17238
+ }));
17239
+ return { pick: (position) => prepared.groups.map((group) => templates.get(BlockStateResolver.choose(group, position))) };
16679
17240
  }
16680
- /** Resolves section templates once, with a synchronous weighted choice for each cell. */
16681
- async prepareState(blockState3, properties = {}) {
17241
+ async state(blockState3, properties) {
16682
17242
  let states = this.states.get(blockState3);
16683
17243
  if (!states) this.states.set(blockState3, states = /* @__PURE__ */ new Map());
16684
17244
  const key = JSON.stringify(Object.entries(properties).sort(([a], [b]) => a.localeCompare(b)));
16685
- const prepared = await cached(states, key, () => this.prepare(blockState3, properties));
16686
- if (!prepared) return void 0;
16687
- return { pick: (position) => prepared.groups.map((group) => prepared.templates.get(BlockStateResolver.choose(group, position))) };
17245
+ return cached(states, key, () => this.prepare(blockState3, properties));
16688
17246
  }
16689
17247
  /** Returns the shared still/flow atlas used by per-block fluid objects. */
16690
17248
  fluidAtlas(kind, root) {
16691
- return cached(this.fluidAtlases, kind, async () => {
17249
+ const key = new AssetKey("minecraft", kind, "models", "fluid", "assets", ".json", root);
17250
+ return cached(this.fluidAtlases, key.serialize(), async () => {
16692
17251
  const atlas = await UVMapper.getAtlas({
16693
- key: new AssetKey("minecraft", kind, "models", "fluid", "assets", ".json", root),
17252
+ key,
16694
17253
  textures: { still: `minecraft:block/${kind}_still`, flow: `minecraft:block/${kind}_flow` },
16695
17254
  elements: []
16696
17255
  });
@@ -16702,64 +17261,67 @@ var SectionModels = class {
16702
17261
  const state = { ...await BlockStateResolver.defaults(blockState3), ...properties };
16703
17262
  const groups = BlockStateResolver.matching(blockState3, state);
16704
17263
  if (!groups.length) return void 0;
16705
- const templates = /* @__PURE__ */ new Map();
17264
+ const prepared = /* @__PURE__ */ new Map();
16706
17265
  for (const group of groups) {
16707
17266
  const choices = Array.isArray(group) ? group : [group];
16708
17267
  if (!choices.length) return void 0;
16709
17268
  for (const variant of choices) {
16710
17269
  if (!variant.model) return void 0;
16711
- const model = await Models.getMerged(AssetKey.parse("models", variant.model));
17270
+ const modelKey = AssetKey.parse("models", variant.model);
17271
+ modelKey.root = blockState3.key?.root;
17272
+ const model = await Models.getMerged(modelKey);
16712
17273
  if (!model?.elements?.length) return void 0;
16713
17274
  const atlas = await UVMapper.getAtlas(model);
16714
17275
  const rotation = BlockStateResolver.rotation(variant);
16715
17276
  if (!atlas || atlas.image.width > this.maxAtlasSize || atlas.image.height > this.maxAtlasSize) return void 0;
16716
- const tints = await BlockTints.get(blockState3.key, state, model);
16717
- let models = this.models.get(model);
16718
- if (!models) this.models.set(model, models = /* @__PURE__ */ new Map());
16719
- const key = JSON.stringify([rotation.x, rotation.y, rotation.z, variant.uvlock, tints]);
16720
- const template = await cached(models, key, async () => {
16721
- const object4 = new SectionModel(model, {
16722
- instanceMeshes: false,
16723
- mergeMeshes: true,
16724
- tints,
16725
- uvLockRotation: variant.uvlock && (rotation.x !== 0 || rotation.y !== 0) ? [rotation.x, rotation.y, rotation.z] : void 0
16726
- });
16727
- await object4.init();
16728
- let geometry = object4.children[0].geometry;
16729
- geometry.applyMatrix4(new Matrix412().makeRotationFromEuler(rotation));
16730
- object4.clear();
16731
- const keep = [];
16732
- const cullFaces = [];
16733
- for (const element of atlas.model.elements) {
16734
- for (const face of CUBE_FACES) {
16735
- const data = element.faces[face];
16736
- keep.push(data !== void 0);
16737
- if (!data) continue;
16738
- const local = CUBE_FACES.findIndex((name) => name === data.cullface);
16739
- if (local < 0) {
16740
- cullFaces.push(0);
16741
- continue;
16742
- }
16743
- const normal = new Vector315(...CUBE_FACE_OFFSETS[local]).applyEuler(rotation);
16744
- const world = CUBE_FACE_OFFSETS.findIndex(([x, y, z]) => Math.abs(normal.x - x) < 1e-6 && Math.abs(normal.y - y) < 1e-6 && Math.abs(normal.z - z) < 1e-6);
16745
- cullFaces.push(world < 0 ? 0 : 1 << world);
16746
- }
16747
- }
16748
- geometry = compactQuads(geometry, keep);
16749
- const template2 = {
16750
- geometry,
16751
- atlas,
16752
- cullFaces: new Uint8Array(cullFaces),
16753
- occludes: ModelCulling.isOpaqueFullCube(atlas) && ModelCulling.toLocalMask(63, rotation) === 63,
16754
- layer: atlas.hasTranslucency ? 1 : 0
16755
- };
16756
- this.templates.add(template2);
16757
- return template2;
16758
- });
16759
- templates.set(variant, template);
17277
+ prepared.set(variant, { model, atlas, rotation });
16760
17278
  }
16761
17279
  }
16762
- return { groups, templates };
17280
+ return { state, groups, choices: prepared };
17281
+ }
17282
+ async template(variant, { model, atlas, rotation }, tints) {
17283
+ let models = this.models.get(model);
17284
+ if (!models) this.models.set(model, models = /* @__PURE__ */ new Map());
17285
+ const key = JSON.stringify([rotation.x, rotation.y, rotation.z, variant.uvlock, tints]);
17286
+ return cached(models, key, async () => {
17287
+ const object4 = new SectionModel(model, {
17288
+ instanceMeshes: false,
17289
+ mergeMeshes: true,
17290
+ tints,
17291
+ uvLockRotation: variant.uvlock && (rotation.x !== 0 || rotation.y !== 0) ? [rotation.x, rotation.y, rotation.z] : void 0
17292
+ });
17293
+ await object4.init();
17294
+ let geometry = object4.children[0].geometry;
17295
+ geometry.applyMatrix4(new Matrix412().makeRotationFromEuler(rotation));
17296
+ object4.clear();
17297
+ const keep = [];
17298
+ const cullFaces = [];
17299
+ for (const element of atlas.model.elements) {
17300
+ for (const face of CUBE_FACES) {
17301
+ const data = element.faces[face];
17302
+ keep.push(data !== void 0);
17303
+ if (!data) continue;
17304
+ const local = CUBE_FACES.findIndex((name) => name === data.cullface);
17305
+ if (local < 0) {
17306
+ cullFaces.push(0);
17307
+ continue;
17308
+ }
17309
+ const normal = new Vector316(...CUBE_FACE_OFFSETS[local]).applyEuler(rotation);
17310
+ const world = CUBE_FACE_OFFSETS.findIndex(([x, y, z]) => Math.abs(normal.x - x) < 1e-6 && Math.abs(normal.y - y) < 1e-6 && Math.abs(normal.z - z) < 1e-6);
17311
+ cullFaces.push(world < 0 ? 0 : 1 << world);
17312
+ }
17313
+ }
17314
+ geometry = compactQuads(geometry, keep);
17315
+ const template = {
17316
+ geometry,
17317
+ atlas,
17318
+ cullFaces: new Uint8Array(cullFaces),
17319
+ occludes: ModelCulling.isOpaqueFullCube(atlas) && ModelCulling.toLocalMask(63, rotation) === 63,
17320
+ layer: atlas.hasTranslucency ? 1 : 0
17321
+ };
17322
+ this.templates.add(template);
17323
+ return template;
17324
+ });
16763
17325
  }
16764
17326
  /** Disposes prepared geometry and clears this world's template caches. */
16765
17327
  clear() {
@@ -16936,17 +17498,17 @@ var MineRenderWorld = class {
16936
17498
  }
16937
17499
  getBlockAt(posOrX, y, z) {
16938
17500
  if (typeof posOrX == "number") {
16939
- return this.getBlockAt(new Vector316(posOrX, y, z));
17501
+ return this.getBlockAt(new Vector317(posOrX, y, z));
16940
17502
  }
16941
17503
  if (isTripleArray(posOrX)) {
16942
- return this.getBlockAt(new Vector316(posOrX[0], posOrX[1], posOrX[2]));
17504
+ return this.getBlockAt(new Vector317(posOrX[0], posOrX[1], posOrX[2]));
16943
17505
  }
16944
17506
  this.validatePosBounds(posOrX);
16945
17507
  return this.getChunkAt(posOrX)?.getBlockAt(posOrX);
16946
17508
  }
16947
17509
  getBiomeAt(posOrX, y, z) {
16948
- if (typeof posOrX === "number") return this.getBiomeAt(new Vector316(posOrX, y, z));
16949
- if (isTripleArray(posOrX)) return this.getBiomeAt(new Vector316(...posOrX));
17510
+ if (typeof posOrX === "number") return this.getBiomeAt(new Vector317(posOrX, y, z));
17511
+ if (isTripleArray(posOrX)) return this.getBiomeAt(new Vector317(...posOrX));
16950
17512
  this.validatePosBounds(posOrX);
16951
17513
  const cx = Math.floor(posOrX.x / 16), cy = Math.floor(posOrX.y / 16), cz = Math.floor(posOrX.z / 16);
16952
17514
  const lx = posOrX.x - cx * 16, ly = posOrX.y - cy * 16, lz = posOrX.z - cz * 16;
@@ -16954,10 +17516,10 @@ var MineRenderWorld = class {
16954
17516
  }
16955
17517
  async setBlockAt(posOrX, yOrBlock, z, block) {
16956
17518
  if (typeof posOrX == "number") {
16957
- return this.setBlockAt(new Vector316(posOrX, yOrBlock, z), block);
17519
+ return this.setBlockAt(new Vector317(posOrX, yOrBlock, z), block);
16958
17520
  }
16959
17521
  if (isTripleArray(posOrX)) {
16960
- return this.setBlockAt(new Vector316(posOrX[0], posOrX[1], posOrX[2]), yOrBlock);
17522
+ return this.setBlockAt(new Vector317(posOrX[0], posOrX[1], posOrX[2]), yOrBlock);
16961
17523
  }
16962
17524
  return this.placeBlock(posOrX, yOrBlock);
16963
17525
  }
@@ -16968,10 +17530,10 @@ var MineRenderWorld = class {
16968
17530
  }
16969
17531
  async setBlockVisibleAt(posOrX, yOrVisible, z, visible) {
16970
17532
  if (typeof posOrX === "number") {
16971
- return this.setBlockVisibleAt(new Vector316(posOrX, yOrVisible, z), visible);
17533
+ return this.setBlockVisibleAt(new Vector317(posOrX, yOrVisible, z), visible);
16972
17534
  }
16973
17535
  if (isTripleArray(posOrX)) {
16974
- return this.setBlockVisibleAt(new Vector316(...posOrX), yOrVisible);
17536
+ return this.setBlockVisibleAt(new Vector317(...posOrX), yOrVisible);
16975
17537
  }
16976
17538
  this.validatePosBounds(posOrX);
16977
17539
  await this.getChunkAt(posOrX)?.setBlockVisibleAt(posOrX, yOrVisible);
@@ -16997,7 +17559,7 @@ var MineRenderWorld = class {
16997
17559
  const collect = async (positions2) => {
16998
17560
  for (const pos of positions2) changes.set(pos.toArray().join(","), pos);
16999
17561
  };
17000
- const place = (block) => this.placeBlock(new Vector316(...block.position), block, collect);
17562
+ const place = (block) => this.placeBlock(new Vector317(...block.position), block, collect);
17001
17563
  const keys = /* @__PURE__ */ new Map();
17002
17564
  const types = /* @__PURE__ */ new Set();
17003
17565
  for (const block of multiblock.blocks) {
@@ -17013,7 +17575,7 @@ var MineRenderWorld = class {
17013
17575
  }
17014
17576
  if (!executor) {
17015
17577
  const groups2 = /* @__PURE__ */ new Map();
17016
- const position = new Vector316();
17578
+ const position = new Vector317();
17017
17579
  for (const block of multiblock.blocks) {
17018
17580
  const [x, y, z] = block.position;
17019
17581
  this.validatePosBounds(position.set(x, y, z));
@@ -17053,7 +17615,7 @@ var MineRenderWorld = class {
17053
17615
  try {
17054
17616
  positions = await chunk.placeBlocks(blocks);
17055
17617
  } finally {
17056
- positions ??= blocks.map(({ index }) => new Vector316(
17618
+ positions ??= blocks.map(({ index }) => new Vector317(
17057
17619
  chunk.x * 16 + index % 16,
17058
17620
  chunk.y * 16 + Math.floor(index / 256),
17059
17621
  chunk.z * 16 + Math.floor(index / 16) % 16
@@ -17089,10 +17651,9 @@ var MineRenderWorld = class {
17089
17651
  }
17090
17652
  try {
17091
17653
  await this.clearChunkColumn(chunk.x, chunk.z);
17092
- let sliceStart = performance.now();
17093
17654
  for (const section of chunk.sections) {
17094
17655
  if (section.biomes) {
17095
- const position = new Vector316(chunk.x * 16, section.y * 16, chunk.z * 16);
17656
+ const position = new Vector317(chunk.x * 16, section.y * 16, chunk.z * 16);
17096
17657
  this.validatePosBounds(position);
17097
17658
  const copy = [...section.biomes];
17098
17659
  if (copy.length !== 64 || !copy.every((id) => typeof id === "string" && id.length > 0)) {
@@ -17103,9 +17664,12 @@ var MineRenderWorld = class {
17103
17664
  if (!column) this.biomes.set(key, column = /* @__PURE__ */ new Map());
17104
17665
  column.set(section.y, copy);
17105
17666
  }
17667
+ }
17668
+ let sliceStart = performance.now();
17669
+ for (const section of chunk.sections) {
17106
17670
  if (!executor) {
17107
17671
  if (!occupiedSections.has(section.y)) continue;
17108
- const position = new Vector316(chunk.x * 16, section.y * 16, chunk.z * 16);
17672
+ const position = new Vector317(chunk.x * 16, section.y * 16, chunk.z * 16);
17109
17673
  this.validatePosBounds(position);
17110
17674
  const target = this.getOrCreateChunkAt(position);
17111
17675
  try {
@@ -17199,7 +17763,7 @@ var MineRenderWorld = class {
17199
17763
  this.sectionModels?.clear();
17200
17764
  }
17201
17765
  updateCulling(positions) {
17202
- const neighbor = new Vector316();
17766
+ const neighbor = new Vector317();
17203
17767
  for (const pos of positions) {
17204
17768
  const cx = Math.floor(pos.x / 16), cy = Math.floor(pos.y / 16), cz = Math.floor(pos.z / 16);
17205
17769
  const chunk = this._chunks.get(`${cx}_${cy}_${cz}`);
@@ -17267,18 +17831,34 @@ var MineRenderWorld = class {
17267
17831
  const key = this.worldPosToChunkKey(pos);
17268
17832
  let chunk = this._chunks.get(key);
17269
17833
  if (typeof chunk === "undefined") {
17834
+ const x = Math.floor(pos.x / 16), y = Math.floor(pos.y / 16), z = Math.floor(pos.z / 16);
17270
17835
  chunk = new Chunk(
17271
17836
  this.scene,
17272
- Math.floor(pos.x / 16),
17273
- Math.floor(pos.y / 16),
17274
- Math.floor(pos.z / 16),
17837
+ x,
17838
+ y,
17839
+ z,
17275
17840
  (positions) => this.updateCulling(positions),
17276
17841
  this.sectionModels,
17277
- (x, y, z) => {
17278
- const cx = Math.floor(x / 16), cy = Math.floor(y / 16), cz = Math.floor(z / 16);
17842
+ (x2, y2, z2) => {
17843
+ const cx = Math.floor(x2 / 16), cy = Math.floor(y2 / 16), cz = Math.floor(z2 / 16);
17279
17844
  const section = this._chunks.get(`${cx}_${cy}_${cz}`);
17280
- const index = (y - cy * 16) * 256 + (z - cz * 16) * 16 + x - cx * 16;
17845
+ const index = (y2 - cy * 16) * 256 + (z2 - cz * 16) * 16 + x2 - cx * 16;
17281
17846
  return (section?.fluidByteIndex(index) ?? 0) | (section?.isOccludingIndex(index) ? 64 : 0);
17847
+ },
17848
+ (x2, y2, z2) => this.getBiomeAt(x2, y2, z2),
17849
+ async (biomes) => {
17850
+ if (this._chunks.get(key) !== chunk) return;
17851
+ const columnKey = `${x}_${z}`;
17852
+ let column = this.biomes.get(columnKey);
17853
+ if (biomes) {
17854
+ if (!column) this.biomes.set(columnKey, column = /* @__PURE__ */ new Map());
17855
+ column.set(y, biomes);
17856
+ } else {
17857
+ column?.delete(y);
17858
+ if (!column?.size) this.biomes.delete(columnKey);
17859
+ }
17860
+ await chunk.refreshBiomes();
17861
+ await this._chunks.get(`${x}_${y + 1}_${z}`)?.refreshBiomes();
17282
17862
  }
17283
17863
  );
17284
17864
  this._chunks.set(key, chunk);
@@ -17291,7 +17871,7 @@ var MineRenderWorld = class {
17291
17871
  }
17292
17872
  /** Converts block coordinates to scene units by multiplying each component by 16. */
17293
17873
  static worldToScenePosition(pos) {
17294
- return new Vector316(
17874
+ return new Vector317(
17295
17875
  pos.x * 16,
17296
17876
  pos.y * 16,
17297
17877
  pos.z * 16
@@ -17299,7 +17879,7 @@ var MineRenderWorld = class {
17299
17879
  }
17300
17880
  /** Converts scene units to block coordinates without rounding to an integer block. */
17301
17881
  static sceneToWorldPosition(pos) {
17302
- return new Vector316(
17882
+ return new Vector317(
17303
17883
  pos.x / 16,
17304
17884
  pos.y / 16,
17305
17885
  pos.z / 16
@@ -17333,6 +17913,7 @@ function buildSectionGeometry(input) {
17333
17913
  if (!Number.isInteger(maxAtlasSize) || maxAtlasSize < 1) throw new RangeError("Section atlas size must be a positive integer");
17334
17914
  if (count > indices.length || count > templates.length || count > cullMasks.length) throw new RangeError("Section entry count exceeds its arrays");
17335
17915
  if (fluids && fluids.cells.length !== 5832) throw new RangeError("Section fluid cells must cover an 18\xD718\xD718 neighborhood");
17916
+ if (fluids?.waterTints && fluids.waterTints.length !== 4096 * 3) throw new RangeError("Section water tints must contain RGB for 4096 cells");
17336
17917
  for (const template of templateData) {
17337
17918
  const { quads } = template;
17338
17919
  if (template.positions.length !== quads * 12 || template.normals.length !== quads * 12 || template.uvs.length !== quads * 8 || template.uvBounds.length !== quads * 16 || template.colors.length !== quads * 12 || template.indices.length !== quads * 6 || template.cullFaces.length !== quads) {
@@ -17410,7 +17991,9 @@ function buildSectionGeometry(input) {
17410
17991
  height
17411
17992
  });
17412
17993
  }
17413
- fluidPages.get(fluid).fluids.push({ quads, x, y, z, data });
17994
+ const tintIndex = (x + z * 16 + y * 256) * 3;
17995
+ const tint = fluid === "water" && fluids.waterTints ? fluids.waterTints.subarray(tintIndex, tintIndex + 3) : data.tint;
17996
+ fluidPages.get(fluid).fluids.push({ quads, x, y, z, data, tint });
17414
17997
  }
17415
17998
  pages.push(...[...fluidPages.values()].sort((a, b) => Number(atlases[a.placements[0].atlas].animated) - Number(atlases[b.placements[0].atlas].animated) || a.placements[0].atlas - b.placements[0].atlas));
17416
17999
  }
@@ -17459,7 +18042,7 @@ function buildSectionGeometry(input) {
17459
18042
  for (let corner = 0; corner < 6; corner++) outputIndices[indexOffset++] = start + template.indices[quad * 6 + corner];
17460
18043
  }
17461
18044
  }
17462
- for (const { quads, x, y, z, data } of page.fluids) {
18045
+ for (const { quads, x, y, z, data, tint } of page.fluids) {
17463
18046
  const atlas = atlases[data.atlas];
17464
18047
  const placement = page.placements[0];
17465
18048
  const mapU = (u) => (placement.x + u * atlas.width) / page.width;
@@ -17483,7 +18066,7 @@ function buildSectionGeometry(input) {
17483
18066
  mapU((rect[0] + rect[2]) / atlas.width),
17484
18067
  mapV(1 - rect[1] / atlas.height)
17485
18068
  ], vertex * 4);
17486
- colors.set(data.tint, vertex * 3);
18069
+ colors.set(tint, vertex * 3);
17487
18070
  }
17488
18071
  outputIndices.set([start, start + 1, start + 2, start, start + 2, start + 3], indexOffset);
17489
18072
  indexOffset += 6;
@@ -17625,7 +18208,7 @@ function toInput(entries, maxAtlasSize, fluids) {
17625
18208
  input.cullMasks[i] = cullMask;
17626
18209
  }
17627
18210
  if (fluids) {
17628
- input.fluids = { cells: fluids.cells };
18211
+ input.fluids = { cells: fluids.cells, waterTints: fluids.waterTints };
17629
18212
  for (const kind of ["water", "lava"]) {
17630
18213
  const atlas = fluids[kind];
17631
18214
  if (!atlas) continue;
@@ -17726,17 +18309,20 @@ var SectionMesh = class _SectionMesh extends Group3 {
17726
18309
  init_FluidGeometry();
17727
18310
  init_FluidQuads();
17728
18311
  init_CubeFace();
18312
+ var WATER_TINT2 = new Color13(4159204).toArray();
17729
18313
  var Chunk = class _Chunk {
17730
- constructor(scene, x, y, z, onBlocksChanged, sectionModels, neighborCell) {
18314
+ constructor(scene, x, y, z, onBlocksChanged, sectionModels, neighborCell, biomeAt, onBiomesChanged) {
17731
18315
  this.onBlocksChanged = onBlocksChanged;
17732
18316
  this.sectionModels = sectionModels;
17733
18317
  this.neighborCell = neighborCell;
18318
+ this.biomeAt = biomeAt;
18319
+ this.onBiomesChanged = onBiomesChanged;
17734
18320
  this.data = new ChunkData();
17735
18321
  this.renderedBlocks = /* @__PURE__ */ new Map();
17736
18322
  this.sectionBlocks = /* @__PURE__ */ new Map();
17737
18323
  this.hiddenBlocks = /* @__PURE__ */ new Set();
17738
18324
  this.fluidCells = new Uint8Array(4096);
17739
- this.blockPosition = new Vector317();
18325
+ this.blockPosition = new Vector318();
17740
18326
  this.meshDirty = false;
17741
18327
  this.meshGeneration = 0;
17742
18328
  this.scene = scene;
@@ -17746,21 +18332,71 @@ var Chunk = class _Chunk {
17746
18332
  }
17747
18333
  getBlockAt(posOrX, y, z) {
17748
18334
  if (typeof posOrX == "number") {
17749
- return this.getBlockAt(new Vector317(posOrX, y, z));
18335
+ return this.getBlockAt(new Vector318(posOrX, y, z));
17750
18336
  }
17751
18337
  if (isTripleArray(posOrX)) {
17752
- return this.getBlockAt(new Vector317(posOrX[0], posOrX[1], posOrX[2]));
18338
+ return this.getBlockAt(new Vector318(posOrX[0], posOrX[1], posOrX[2]));
17753
18339
  }
17754
18340
  const pos = this.blockPosition.set(posOrX.x - this.x * 16, posOrX.y - this.y * 16, posOrX.z - this.z * 16);
17755
18341
  const index = _Chunk.chunkPosToBlockIndex(pos);
17756
18342
  return this.renderedBlocks.get(index);
17757
18343
  }
18344
+ getBiomeAt(posOrX, y, z) {
18345
+ if (typeof posOrX === "number") return this.getBiomeAt(new Vector318(posOrX, y, z));
18346
+ if (isTripleArray(posOrX)) return this.getBiomeAt(new Vector318(...posOrX));
18347
+ const pos = this.worldPosToChunkPos(posOrX);
18348
+ _Chunk.chunkPosToBlockIndex(pos);
18349
+ return this.biomeAt ? this.biomeAt(posOrX.x, posOrX.y, posOrX.z) : this.biomes?.[Math.floor(pos.x / 4) + Math.floor(pos.z / 4) * 4 + Math.floor(pos.y / 4) * 16];
18350
+ }
18351
+ /** Copies 64 biome IDs in x + z * 4 + y * 16 order and refreshes placed blocks. Omit the array to clear biome data. */
18352
+ setBiomes(biomes) {
18353
+ const copy = biomes ? [...biomes] : void 0;
18354
+ if (copy && (copy.length !== 64 || !copy.every((id) => typeof id === "string" && id.length > 0))) {
18355
+ throw new RangeError("Section biomes must contain 64 nonempty biome IDs");
18356
+ }
18357
+ if (this.onBiomesChanged) return this.onBiomesChanged(copy);
18358
+ this.biomes = copy;
18359
+ return this.refreshBiomes();
18360
+ }
18361
+ /** Rebuilds biome-tinted blocks while preserving hidden blocks and unrelated render objects. */
18362
+ async refreshBiomes() {
18363
+ const affected = /* @__PURE__ */ new Map();
18364
+ const blocks = [];
18365
+ for (const index of this.renderedBlocks.keys()) {
18366
+ const block = this.data.get(index);
18367
+ const stateKey = ChunkData.paletteKey(block);
18368
+ let matches = affected.get(stateKey);
18369
+ if (!matches) {
18370
+ const key = AssetKey.parse("blockstates", block.type);
18371
+ matches = BlockStates.get(key).then(async (state) => {
18372
+ const origin = state?.key ?? key;
18373
+ const [source, fluid] = await Promise.all([BlockTints.biomeSource(origin), getBlockFluidState(origin, block.properties)]);
18374
+ return !!source || fluid?.kind === "water";
18375
+ });
18376
+ affected.set(stateKey, matches);
18377
+ }
18378
+ if (await matches) blocks.push({ index, block });
18379
+ }
18380
+ if (!blocks.length) return;
18381
+ const hidden = new Set(this.hiddenBlocks);
18382
+ try {
18383
+ await this.placeBlocks(blocks);
18384
+ } finally {
18385
+ for (const index of hidden) {
18386
+ this.hiddenBlocks.add(index);
18387
+ this.renderedBlocks.get(index)?.object?.setVisible(false);
18388
+ }
18389
+ const positions = blocks.map(({ index }) => new Vector318(...this.positionAt(index)));
18390
+ if (this.onBlocksChanged) await this.onBlocksChanged(positions);
18391
+ else await this.rebuildSectionMesh();
18392
+ }
18393
+ }
17758
18394
  async setBlockAt(posOrX, yOrBlock, z, block) {
17759
18395
  if (typeof posOrX == "number") {
17760
- return this.setBlockAt(new Vector317(posOrX, yOrBlock, z), block);
18396
+ return this.setBlockAt(new Vector318(posOrX, yOrBlock, z), block);
17761
18397
  }
17762
18398
  if (isTripleArray(posOrX)) {
17763
- return this.setBlockAt(new Vector317(posOrX[0], posOrX[1], posOrX[2]), yOrBlock);
18399
+ return this.setBlockAt(new Vector318(posOrX[0], posOrX[1], posOrX[2]), yOrBlock);
17764
18400
  }
17765
18401
  const worldPos = posOrX;
17766
18402
  const pos = this.worldPosToChunkPos(worldPos);
@@ -17793,14 +18429,15 @@ var Chunk = class _Chunk {
17793
18429
  nbt: block.nbt === void 0 ? void 0 : structuredClone(block.nbt)
17794
18430
  } : void 0
17795
18431
  }));
17796
- const states = new Map(placements.filter(({ key }) => key !== void 0).map(({ key, block }) => [key, block]));
17797
- const settled = await Promise.allSettled([...states.values()].map(async (stored) => {
17798
- const blockState3 = await BlockStates.get(AssetKey.parse("blockstates", stored.type));
17799
- const perBlock = !blockState3 || !this.sectionModels || !!(blockState3.key && BlockEntities.entry(await BlockEntities.getIndex(blockState3.key.root), blockState3.key.toNamespacedString()));
17800
- const fluid = await getBlockFluidState(blockState3?.key, stored.properties);
17801
- const prepared = perBlock || fluid?.renderModel === false ? void 0 : await this.sectionModels.prepareState(blockState3, stored.properties);
17802
- return { blockState: blockState3, perBlock, fluid, prepared, stored };
17803
- }));
18432
+ const states = /* @__PURE__ */ new Map();
18433
+ for (const { index, key, block } of placements) {
18434
+ if (key === void 0) continue;
18435
+ let state = states.get(key);
18436
+ if (!state) states.set(key, state = { stored: block, indices: [] });
18437
+ state.indices.push(index);
18438
+ }
18439
+ const biomes = /* @__PURE__ */ new Map();
18440
+ const settled = await Promise.allSettled([...states.values()].map(({ stored, indices }) => this.resolveState(stored, indices, biomes)));
17804
18441
  const resolutions = new Map([...states.keys()].map((key, i) => [key, settled[i]]));
17805
18442
  const positions = [];
17806
18443
  let failed = false, failure;
@@ -17813,7 +18450,7 @@ var Chunk = class _Chunk {
17813
18450
  if (this.fluidCells[index]) this.meshDirty = true;
17814
18451
  this.fluidCells[index] = 0;
17815
18452
  this.renderedBlocks.delete(index);
17816
- const worldPos = new Vector317(
18453
+ const worldPos = new Vector318(
17817
18454
  this.x * 16 + index % 16,
17818
18455
  this.y * 16 + Math.floor(index / 256),
17819
18456
  this.z * 16 + Math.floor(index / 16) % 16
@@ -17824,7 +18461,8 @@ var Chunk = class _Chunk {
17824
18461
  if (!readBlock) continue;
17825
18462
  const resolved = resolutions.get(key);
17826
18463
  if (resolved.status === "rejected") throw resolved.reason;
17827
- const { blockState: blockState3, perBlock, fluid, prepared, stored } = resolved.value;
18464
+ const { blockState: blockState3, perBlock, fluid, prepared, contexts, stored } = resolved.value;
18465
+ const context = contexts.get(index);
17828
18466
  if (!blockState3) {
17829
18467
  this.data.assign(index, 0);
17830
18468
  continue;
@@ -17832,9 +18470,10 @@ var Chunk = class _Chunk {
17832
18470
  const variantPosition = worldPos.toArray();
17833
18471
  if (fluid) {
17834
18472
  this.fluidCells[index] = (fluid.kind === "water" ? 16 : 32) | Math.min(fluid.level, 8);
18473
+ if (fluid.kind === "water") this.setWaterTint(index, context?.waterTint);
17835
18474
  this.meshDirty = true;
17836
18475
  }
17837
- const templates = perBlock ? void 0 : fluid?.renderModel === false ? [] : prepared?.pick(variantPosition);
18476
+ const templates = perBlock ? void 0 : fluid?.renderModel === false ? [] : (context?.prepared ?? prepared)?.pick(variantPosition);
17838
18477
  if (templates) {
17839
18478
  this.sectionBlocks.set(index, templates.map((template) => ({ index, template, cullMask: 0 })));
17840
18479
  this.meshDirty = true;
@@ -17844,7 +18483,9 @@ var Chunk = class _Chunk {
17844
18483
  instanceMeshes: true,
17845
18484
  maxInstanceCount: 2e3,
17846
18485
  initialState: stored.properties,
17847
- variantPosition
18486
+ variantPosition,
18487
+ biome: context?.biome,
18488
+ biomePosition: this.biomePosition(index, blockState3, stored)
17848
18489
  });
17849
18490
  object4.setPosition(MineRenderWorld.worldToScenePosition(worldPos));
17850
18491
  }
@@ -17867,24 +18508,21 @@ var Chunk = class _Chunk {
17867
18508
  for (const info of this.renderedBlocks.values()) info.object?.removeFromScene();
17868
18509
  this.sectionBlocks.clear();
17869
18510
  this.fluidCells.fill(0);
18511
+ this.waterTints = void 0;
17870
18512
  this.renderedBlocks.clear();
17871
18513
  this.hiddenBlocks.clear();
17872
18514
  this.meshDirty = true;
17873
18515
  this.data.copyFrom(data);
17874
- const ids = /* @__PURE__ */ new Set();
18516
+ const ids = /* @__PURE__ */ new Map();
17875
18517
  for (let index = 0; index < 4096; index++) {
17876
18518
  const id = this.data.idAt(index);
17877
- if (id) ids.add(id);
17878
- }
17879
- const palette = [...ids];
17880
- const settled = await Promise.allSettled(palette.map(async (id) => {
17881
- const state = this.data.stateAt(id);
17882
- const blockState3 = await BlockStates.get(AssetKey.parse("blockstates", state.type));
17883
- const perBlock = !blockState3 || !this.sectionModels || !!(blockState3.key && BlockEntities.entry(await BlockEntities.getIndex(blockState3.key.root), blockState3.key.toNamespacedString()));
17884
- const fluid = await getBlockFluidState(blockState3?.key, state.properties);
17885
- const prepared = perBlock || fluid?.renderModel === false ? void 0 : await this.sectionModels.prepareState(blockState3, state.properties);
17886
- return { blockState: blockState3, perBlock, fluid, prepared };
17887
- }));
18519
+ if (!id) continue;
18520
+ let indices = ids.get(id);
18521
+ if (!indices) ids.set(id, indices = []);
18522
+ indices.push(index);
18523
+ }
18524
+ const palette = [...ids.keys()], biomes = /* @__PURE__ */ new Map();
18525
+ const settled = await Promise.allSettled(palette.map((id) => this.resolveState(this.data.stateAt(id), ids.get(id), biomes)));
17888
18526
  const resolutions = new Map(palette.map((id, i) => [id, settled[i]]));
17889
18527
  const perBlocks = [];
17890
18528
  let failed = false, failure;
@@ -17894,24 +18532,28 @@ var Chunk = class _Chunk {
17894
18532
  try {
17895
18533
  const resolved = resolutions.get(id);
17896
18534
  if (resolved.status === "rejected") throw resolved.reason;
17897
- const { blockState: blockState3, perBlock, fluid, prepared } = resolved.value;
18535
+ const { blockState: blockState3, perBlock, fluid, prepared, contexts } = resolved.value;
18536
+ const context = contexts.get(index);
17898
18537
  if (!blockState3) {
17899
18538
  this.data.assign(index, 0);
17900
18539
  this.fluidCells[index] = 0;
17901
18540
  continue;
17902
18541
  }
17903
- if (fluid) this.fluidCells[index] = (fluid.kind === "water" ? 16 : 32) | Math.min(fluid.level, 8);
18542
+ if (fluid) {
18543
+ this.fluidCells[index] = (fluid.kind === "water" ? 16 : 32) | Math.min(fluid.level, 8);
18544
+ if (fluid.kind === "water") this.setWaterTint(index, context?.waterTint);
18545
+ }
17904
18546
  const variantPosition = [
17905
18547
  this.x * 16 + index % 16,
17906
18548
  this.y * 16 + Math.floor(index / 256),
17907
18549
  this.z * 16 + Math.floor(index / 16) % 16
17908
18550
  ];
17909
- const templates = perBlock ? void 0 : fluid?.renderModel === false ? [] : prepared?.pick(variantPosition);
18551
+ const templates = perBlock ? void 0 : fluid?.renderModel === false ? [] : (context?.prepared ?? prepared)?.pick(variantPosition);
17910
18552
  if (templates) {
17911
18553
  this.sectionBlocks.set(index, templates.map((template) => ({ index, template, cullMask: 0 })));
17912
18554
  this.meshDirty = true;
17913
18555
  } else {
17914
- perBlocks.push({ index, blockState: blockState3 });
18556
+ perBlocks.push({ index, blockState: blockState3, biome: context?.biome });
17915
18557
  }
17916
18558
  const readBlock = this.data.snapshot(index);
17917
18559
  this.renderedBlocks.set(index, { get block() {
@@ -17924,7 +18566,7 @@ var Chunk = class _Chunk {
17924
18566
  failed = true;
17925
18567
  }
17926
18568
  }
17927
- for (const { index, blockState: blockState3 } of perBlocks) {
18569
+ for (const { index, blockState: blockState3, biome } of perBlocks) {
17928
18570
  let object4;
17929
18571
  try {
17930
18572
  object4 = await this.scene.addBlock(blockState3, {
@@ -17932,13 +18574,11 @@ var Chunk = class _Chunk {
17932
18574
  instanceMeshes: true,
17933
18575
  maxInstanceCount: 2e3,
17934
18576
  initialState: this.data.stateAt(this.data.idAt(index)).properties,
17935
- variantPosition: [
17936
- this.x * 16 + index % 16,
17937
- this.y * 16 + Math.floor(index / 256),
17938
- this.z * 16 + Math.floor(index / 16) % 16
17939
- ]
18577
+ variantPosition: this.positionAt(index),
18578
+ biome,
18579
+ biomePosition: this.biomePosition(index, blockState3, this.data.stateAt(this.data.idAt(index)))
17940
18580
  });
17941
- object4.setPosition(new Vector317(
18581
+ object4.setPosition(new Vector318(
17942
18582
  this.x * 256 + index % 16 * 16,
17943
18583
  this.y * 256 + Math.floor(index / 256) * 16,
17944
18584
  this.z * 256 + Math.floor(index / 16) % 16 * 16
@@ -17955,6 +18595,68 @@ var Chunk = class _Chunk {
17955
18595
  }
17956
18596
  if (failed) throw failure;
17957
18597
  }
18598
+ positionAt(index) {
18599
+ return [this.x * 16 + index % 16, this.y * 16 + Math.floor(index / 256), this.z * 16 + Math.floor(index / 16) % 16];
18600
+ }
18601
+ biomePosition(index, blockState3, stored) {
18602
+ const position = this.positionAt(index), name = blockState3.key?.toNamespacedString();
18603
+ if (stored.properties?.half === "upper" && (name === "minecraft:tall_grass" || name === "minecraft:large_fern")) position[1]--;
18604
+ return position;
18605
+ }
18606
+ async resolveState(stored, indices, biomes) {
18607
+ const blockState3 = await BlockStates.get(AssetKey.parse("blockstates", stored.type));
18608
+ const perBlock = !blockState3 || !this.sectionModels || !!(blockState3.key && BlockEntities.entry(await BlockEntities.getIndex(blockState3.key.root), blockState3.key.toNamespacedString()));
18609
+ const fluid = await getBlockFluidState(blockState3?.key, stored.properties);
18610
+ const hasBiomes = this.getBiomeAt(this.x * 16, this.y * 16, this.z * 16) !== void 0 || this.biomeAt?.(this.x * 16, this.y * 16 - 1, this.z * 16) !== void 0;
18611
+ const source = hasBiomes && blockState3 ? await BlockTints.biomeSource(blockState3.key) : void 0;
18612
+ const prepare = (color2) => perBlock || fluid?.renderModel === false ? void 0 : this.sectionModels.prepareState(blockState3, stored.properties, color2);
18613
+ const contexts = /* @__PURE__ */ new Map();
18614
+ const groups = /* @__PURE__ */ new Map();
18615
+ if (blockState3 && (source || fluid?.kind === "water")) {
18616
+ for (const index of indices) {
18617
+ const position = this.biomePosition(index, blockState3, stored);
18618
+ const biome = this.biomeAt ? this.biomeAt(...position) : position[1] >= this.y * 16 ? this.getBiomeAt(position) : void 0;
18619
+ if (!biome) continue;
18620
+ const key = JSON.stringify(source === "grass" ? [biome, position[0], position[2]] : [biome]);
18621
+ let group = groups.get(key);
18622
+ if (!group) groups.set(key, group = { biome, position, indices: [] });
18623
+ group.indices.push(index);
18624
+ }
18625
+ }
18626
+ const groupedCount = [...groups.values()].reduce((sum, group) => sum + group.indices.length, 0);
18627
+ const prepared = groupedCount < indices.length ? await prepare() : void 0;
18628
+ await Promise.all([...groups.values()].map(async ({ biome, position, indices: indices2 }) => {
18629
+ const available = () => {
18630
+ const key = JSON.stringify([biome, blockState3?.key?.root]);
18631
+ let found = biomes.get(key);
18632
+ if (!found) {
18633
+ found = Biomes.get(biome, blockState3?.key).then((value) => value !== void 0);
18634
+ biomes.set(key, found);
18635
+ }
18636
+ return found;
18637
+ };
18638
+ const context = { biome };
18639
+ if (perBlock) {
18640
+ if (!await available()) context.biome = void 0;
18641
+ } else {
18642
+ context.prepared = await prepare(async () => await available() ? BlockTints.getBiomeColor(blockState3?.key, biome, position) : void 0);
18643
+ if (fluid?.kind === "water" && await available()) {
18644
+ const color2 = await Biomes.getColor(biome, "water", blockState3?.key);
18645
+ if (color2 !== void 0 && color2 !== 4159204) context.waterTint = new Color13(color2).toArray();
18646
+ }
18647
+ }
18648
+ for (const index of indices2) contexts.set(index, context);
18649
+ }));
18650
+ return { blockState: blockState3, perBlock, fluid, prepared, contexts, stored };
18651
+ }
18652
+ setWaterTint(index, tint) {
18653
+ if (!this.sectionModels || !this.waterTints && tint === void 0) return;
18654
+ if (!this.waterTints) {
18655
+ this.waterTints = new Float32Array(4096 * 3);
18656
+ for (let cell = 0; cell < 4096; cell++) this.waterTints.set(WATER_TINT2, cell * 3);
18657
+ }
18658
+ this.waterTints.set(tint ?? WATER_TINT2, index * 3);
18659
+ }
17958
18660
  /** Changes visibility at world block coordinates while preserving the block's data. */
17959
18661
  async setBlockVisibleAt(pos, visible) {
17960
18662
  const index = _Chunk.chunkPosToBlockIndex(this.worldPosToChunkPos(pos));
@@ -18058,7 +18760,7 @@ var Chunk = class _Chunk {
18058
18760
  if (block && !block.object) origins.set(kind, block.block.type);
18059
18761
  }
18060
18762
  if (origins.size) {
18061
- fluids = { cells: new Uint8Array(18 * 18 * 18) };
18763
+ fluids = { cells: new Uint8Array(18 * 18 * 18), waterTints: this.waterTints?.slice() };
18062
18764
  for (let y = -1; y <= 16; y++) {
18063
18765
  for (let z = -1; z <= 16; z++) {
18064
18766
  for (let x = -1; x <= 16; x++) {
@@ -18089,7 +18791,7 @@ var Chunk = class _Chunk {
18089
18791
  /** Removes stored blocks and render objects. Set `notify` to false to skip the owning world's update. */
18090
18792
  async clear(notify = true) {
18091
18793
  const positions = notify && this.onBlocksChanged ? [...this.renderedBlocks.keys()].map((index) => this.chunkPosToWorldPos(
18092
- new Vector317(index % 16, Math.floor(index / 256), Math.floor(index / 16) % 16)
18794
+ new Vector318(index % 16, Math.floor(index / 256), Math.floor(index / 16) % 16)
18093
18795
  )) : [];
18094
18796
  for (const info of this.renderedBlocks.values()) info.object?.removeFromScene();
18095
18797
  this.sectionBlocks.clear();
@@ -18100,8 +18802,13 @@ var Chunk = class _Chunk {
18100
18802
  this.renderedBlocks.clear();
18101
18803
  this.hiddenBlocks.clear();
18102
18804
  this.fluidCells.fill(0);
18805
+ this.waterTints = void 0;
18103
18806
  this.data.clear();
18104
- if (notify) await this.onBlocksChanged?.(positions);
18807
+ this.biomes = void 0;
18808
+ if (notify) {
18809
+ await this.onBiomesChanged?.(void 0);
18810
+ await this.onBlocksChanged?.(positions);
18811
+ }
18105
18812
  }
18106
18813
  async dispose() {
18107
18814
  await this.clear();
@@ -18117,14 +18824,14 @@ var Chunk = class _Chunk {
18117
18824
  return pos.y * 16 * 16 + pos.z * 16 + pos.x;
18118
18825
  }
18119
18826
  worldPosToChunkPos(pos) {
18120
- return new Vector317(
18827
+ return new Vector318(
18121
18828
  pos.x - this.x * 16,
18122
18829
  pos.y - this.y * 16,
18123
18830
  pos.z - this.z * 16
18124
18831
  );
18125
18832
  }
18126
18833
  chunkPosToWorldPos(pos) {
18127
- return new Vector317(
18834
+ return new Vector318(
18128
18835
  this.x * 16 + pos.x,
18129
18836
  this.y * 16 + pos.y,
18130
18837
  this.z * 16 + pos.z
@@ -18486,6 +19193,7 @@ export {
18486
19193
  BannerPatterns,
18487
19194
  BasicAssetKey,
18488
19195
  BatchedExecutor,
19196
+ Biomes,
18489
19197
  BlockEntities,
18490
19198
  BlockObject,
18491
19199
  BlockStateResolver,
@@ -18517,6 +19225,7 @@ export {
18517
19225
  EntityObject,
18518
19226
  Env,
18519
19227
  FluidModelObject,
19228
+ FlyControls,
18520
19229
  Fonts,
18521
19230
  GUI_CONTAINER_LAYOUTS,
18522
19231
  Geometries,
@@ -18622,6 +19331,7 @@ export {
18622
19331
  isDisposable,
18623
19332
  isDoubleArray,
18624
19333
  isEntityObject,
19334
+ isFlyControls,
18625
19335
  isGuiObject,
18626
19336
  isInstanceManager,
18627
19337
  isInstanceReference,