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.
@@ -279,7 +279,8 @@ var init_Caching = __esm({
279
279
  this.fontCache,
280
280
  this.bannerPatternCache,
281
281
  this.mineRenderDataCache,
282
- this.usernameUuidCache
282
+ this.usernameUuidCache,
283
+ this.biomeCache
283
284
  ];
284
285
  }
285
286
  /** Invalidates in-memory entries so later lookups reload them. Persisted entries are retained. */
@@ -330,6 +331,7 @@ var init_Caching = __esm({
330
331
  _Caching.bannerPatternCache = _Caching.createAssetCache();
331
332
  _Caching.mineRenderDataCache = _Caching.createAssetCache();
332
333
  _Caching.usernameUuidCache = Caches.builder().expireAfterWrite(Time.minutes(5)).expirationInterval(Time.seconds(30)).buildAsync();
334
+ _Caching.biomeCache = _Caching.createAssetCache();
333
335
  Caching = _Caching;
334
336
  }
335
337
  });
@@ -1676,7 +1678,7 @@ ${key}` : key, async () => {
1676
1678
  case "blockStates":
1677
1679
  return Object.values(value).every((property) => object(property) && "default" in property && Array.isArray(property.values));
1678
1680
  case "blockTints":
1679
- return ["constant", "grass", "stem", "redstone"].includes(value.source) && (value.source !== "constant" || Number.isInteger(value.color));
1681
+ 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));
1680
1682
  case "fluids":
1681
1683
  return ["water", "lava"].includes(value.kind) && typeof value.renderModel === "boolean" && (value.levelProperty === void 0 || typeof value.levelProperty === "string");
1682
1684
  case "dyes":
@@ -1820,15 +1822,19 @@ var init_Colormaps = __esm({
1820
1822
  Colormaps = class {
1821
1823
  /** Samples the selected resource pack's grass colormap using Minecraft's climate coordinates. */
1822
1824
  static async grassColor(origin, temperature = 0.5, downfall = 1) {
1823
- const key = AssetKey.parse("textures", "minecraft:colormap/grass", origin);
1825
+ return this.getColor("grass", origin, temperature, downfall);
1826
+ }
1827
+ /** Samples a resource-pack colormap at the supplied climate coordinates. */
1828
+ static async getColor(kind, origin, temperature = 0.5, downfall = 1) {
1829
+ const key = AssetKey.parse("textures", `minecraft:colormap/${kind}`, origin);
1824
1830
  const temp = Math.fround(temperature);
1825
1831
  const rain = Math.fround(downfall) * temp;
1826
1832
  const x = Math.floor((1 - temp) * 255), y = Math.floor((1 - rain) * 255);
1827
1833
  return await Caching.blockTintCache.get(`${key.serialize()}|${x},${y}`, async () => {
1828
1834
  const image = await ModelTextures.get(key);
1829
- if (!image) throw new MineRenderError(`Missing grass colormap: ${key.toNamespacedString()}`);
1835
+ if (!image) throw new MineRenderError(`Missing ${kind} colormap: ${key.toNamespacedString()}`);
1830
1836
  if (image.width !== 256 || image.height !== 256) {
1831
- throw new MineRenderError("Grass colormaps must be 256 \xD7 256 pixels");
1837
+ throw new MineRenderError(`${kind} colormaps must be 256 \xD7 256 pixels`);
1832
1838
  }
1833
1839
  const [red, green, blue] = image.data.getImageData(x, y, 1, 1).data;
1834
1840
  return red << 16 | green << 8 | blue;
@@ -7699,6 +7705,116 @@ init_AssetLoader();
7699
7705
  init_Assets();
7700
7706
  init_BannerPatterns();
7701
7707
 
7708
+ // src/assets/Biomes.ts
7709
+ init_AssetKey();
7710
+ init_AssetLoader();
7711
+ init_AssetParsers();
7712
+ init_Caching();
7713
+ init_MineRenderError();
7714
+ init_Colormaps();
7715
+
7716
+ // src/assets/_biomes/SwampGrassColor.ts
7717
+ var permutation;
7718
+ 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]];
7719
+ var skew = (Math.sqrt(3) - 1) / 2;
7720
+ var unskew = (3 - Math.sqrt(3)) / 6;
7721
+ function createPermutation() {
7722
+ const multiplier = 0x5deece66dn, mask = (1n << 48n) - 1n;
7723
+ let seed = 2345n ^ multiplier;
7724
+ const next = (bits) => {
7725
+ seed = seed * multiplier + 11n & mask;
7726
+ return Number(seed >> BigInt(48 - bits));
7727
+ };
7728
+ for (let i = 0; i < 3; i++) {
7729
+ next(26);
7730
+ next(27);
7731
+ }
7732
+ const values = Uint8Array.from({ length: 256 }, (_, i) => i);
7733
+ for (let i = 0; i < 256; i++) {
7734
+ const bound = 256 - i;
7735
+ let offset;
7736
+ if ((bound & bound - 1) === 0) offset = Math.floor(bound * next(31) / 2147483648);
7737
+ else {
7738
+ let bits;
7739
+ do {
7740
+ bits = next(31);
7741
+ offset = bits % bound;
7742
+ } while (bits - offset + bound - 1 > 2147483647);
7743
+ }
7744
+ const other = i + offset;
7745
+ [values[i], values[other]] = [values[other], values[i]];
7746
+ }
7747
+ return values;
7748
+ }
7749
+ function swampGrassColor(x, z) {
7750
+ const p10 = permutation ?? (permutation = createPermutation());
7751
+ x *= 0.0225;
7752
+ z *= 0.0225;
7753
+ const s = (x + z) * skew;
7754
+ const i = Math.floor(x + s), j = Math.floor(z + s);
7755
+ const t = (i + j) * unskew;
7756
+ const dx = x - (i - t), dz = z - (j - t);
7757
+ const stepX = dx > dz ? 1 : 0, stepZ = 1 - stepX;
7758
+ const corner = (gx, gz, a, b) => {
7759
+ let falloff = 0.5 - a * a - b * b;
7760
+ if (falloff < 0) return 0;
7761
+ const gradient = gradients[p10[gx + p10[gz & 255] & 255] % 12];
7762
+ falloff *= falloff;
7763
+ return falloff * falloff * (gradient[0] * a + gradient[1] * b);
7764
+ };
7765
+ 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));
7766
+ return noise < -0.1 ? 5011004 : 6975545;
7767
+ }
7768
+
7769
+ // src/assets/Biomes.ts
7770
+ var Biomes = class {
7771
+ /** Loads a biome registry entry, or returns `undefined` when no source provides it. */
7772
+ static async get(id, origin) {
7773
+ if (!/^(?:[a-z0-9_.-]+:)?[a-z0-9_./-]+$/.test(id)) throw new MineRenderError(`Invalid biome identifier: ${id}`);
7774
+ const [namespace, path] = id.includes(":") ? id.split(":") : ["minecraft", id];
7775
+ const key = new AssetKey(namespace, path, "worldgen", "biome", "data", ".json", origin?.root);
7776
+ return Caching.biomeCache.get(key.serialize(), async () => {
7777
+ const asset2 = await AssetLoader.get(key, "json" /* JSON */);
7778
+ if (asset2 === void 0) return void 0;
7779
+ if (!asset2 || typeof asset2 !== "object" || Array.isArray(asset2) || !asset2.effects || typeof asset2.effects !== "object" || Array.isArray(asset2.effects)) {
7780
+ throw new MineRenderError(`Invalid biome effects: ${id}`);
7781
+ }
7782
+ if (typeof asset2.temperature !== "number" || !Number.isFinite(asset2.temperature) || typeof asset2.downfall !== "number" || !Number.isFinite(asset2.downfall)) {
7783
+ throw new MineRenderError(`Invalid biome climate: ${id}`);
7784
+ }
7785
+ for (const kind of ["grass", "foliage", "dry_foliage", "water"]) {
7786
+ const color2 = asset2.effects[`${kind}_color`];
7787
+ if (color2 !== void 0) this.parseColor(color2, id, kind);
7788
+ }
7789
+ const modifier = asset2.effects.grass_color_modifier;
7790
+ if (modifier !== void 0 && modifier !== "none" && modifier !== "dark_forest" && modifier !== "swamp") {
7791
+ throw new MineRenderError(`Invalid biome grass color modifier: ${id}`);
7792
+ }
7793
+ return asset2;
7794
+ });
7795
+ }
7796
+ /** Returns packed sRGB, or `undefined` when the biome or requested water color is missing. */
7797
+ static async getColor(id, kind, origin, x = 0, z = 0) {
7798
+ const biome = await this.get(id, origin);
7799
+ if (!biome) return void 0;
7800
+ const rawColor = biome.effects[`${kind}_color`];
7801
+ let color2 = rawColor === void 0 ? void 0 : this.parseColor(rawColor, id, kind);
7802
+ if (kind === "water") return color2;
7803
+ const modifier = kind === "grass" ? biome.effects.grass_color_modifier : void 0;
7804
+ if (modifier === "swamp") return swampGrassColor(x, z);
7805
+ if (color2 === void 0) {
7806
+ const clamp = (value) => Math.max(0, Math.min(1, Math.fround(value)));
7807
+ color2 = await Colormaps.getColor(kind, origin, clamp(biome.temperature), clamp(biome.downfall));
7808
+ }
7809
+ return modifier === "dark_forest" ? (color2 & 16711422) + 2634762 >> 1 : color2;
7810
+ }
7811
+ static parseColor(value, id, kind) {
7812
+ if (typeof value === "string" && /^#[0-9a-fA-F]{6}$/.test(value)) return parseInt(value.slice(1), 16);
7813
+ if (typeof value === "number" && Number.isInteger(value) && value >= 0 && value <= 16777215) return value;
7814
+ throw new MineRenderError(`Invalid biome ${kind} color: ${id}`);
7815
+ }
7816
+ };
7817
+
7702
7818
  // src/assets/BlockEntities.ts
7703
7819
  init_AssetKey();
7704
7820
  init_AssetLoader();
@@ -12178,8 +12294,20 @@ var BlockStateResolver = class _BlockStateResolver {
12178
12294
  init_Colormaps();
12179
12295
  init_MineRenderData();
12180
12296
  var BlockTints = class {
12181
- /** Resolves block preview colors without biome context; explicit tints take precedence. */
12182
- static async get(key, state, model, tints) {
12297
+ /** Returns a block's versioned biome color source, leaving fixed-color blocks unchanged. */
12298
+ static async biomeSource(key) {
12299
+ const rule = key && (await MineRenderData.get("blockTints", key.root))[key.toNamespacedString()];
12300
+ return rule ? rule.biome ?? (rule.source === "grass" ? "grass" : void 0) : void 0;
12301
+ }
12302
+ /** Resolves one block's biome tint at world block coordinates, without biome blending. */
12303
+ static async getBiomeColor(key, biome, position) {
12304
+ if (!biome) return void 0;
12305
+ const x = position?.[0], z = position?.[2];
12306
+ const source = await this.biomeSource(key);
12307
+ return source ? Biomes.getColor(biome, source, key, x, z) : void 0;
12308
+ }
12309
+ /** Resolves used tint indices from biome or preview colors; explicit tints take precedence. */
12310
+ static async get(key, state, model, tints, biomeColor) {
12183
12311
  if (!key) return tints;
12184
12312
  const candidates = (model.elements ?? []).flatMap((element) => Object.values(element.faces)).some((face) => face?.tintindex !== void 0 && face.tintindex >= 0 && tints?.[face.tintindex] === void 0);
12185
12313
  if (!candidates) return tints;
@@ -12195,8 +12323,12 @@ var BlockTints = class {
12195
12323
  }
12196
12324
  }
12197
12325
  if (!indices.size) return tints;
12326
+ const source = rule.biome ?? (rule.source === "grass" ? "grass" : void 0);
12327
+ const biome = source ? typeof biomeColor === "function" ? await biomeColor() : biomeColor : void 0;
12198
12328
  let color2;
12199
- switch (rule.source) {
12329
+ if (biome !== void 0) {
12330
+ color2 = biome;
12331
+ } else switch (rule.source) {
12200
12332
  case "constant":
12201
12333
  color2 = rule.color;
12202
12334
  break;
@@ -12255,6 +12387,7 @@ var _BlockObject = class _BlockObject extends SceneObject {
12255
12387
  this._entityGeneration = 0;
12256
12388
  this.options = (0, import_ts_deepmerge6.default)({}, _BlockObject.DEFAULT_OPTIONS, options ?? {});
12257
12389
  this.variantPosition = options?.variantPosition ? [...options.variantPosition] : void 0;
12390
+ this.biomePosition = options?.biomePosition ? [...options.biomePosition] : void 0;
12258
12391
  }
12259
12392
  async init() {
12260
12393
  const root = AssetLoader.ROOT, scope = AssetLoader.persistentScope;
@@ -12447,11 +12580,16 @@ var _BlockObject = class _BlockObject extends SceneObject {
12447
12580
  const { FluidModelObject: FluidModelObject2, sampleFluid: sampleFluid2 } = await Promise.resolve().then(() => (init_FluidModelObject(), FluidModelObject_exports));
12448
12581
  this._fluidSampler = sample;
12449
12582
  const surface = sampleFluid2(kind, (x, y, z) => x === 0 && y === 0 && z === 0 ? { fluid: kind, level: this.fluidLevel } : sample?.(x, y, z) ?? {});
12450
- if (this._fluidKey === surface.key) return;
12583
+ const tint = this.options.tints?.[0] ?? this.waterTint;
12584
+ const fluidKey = `${surface.key}|${tint ?? "default"}`;
12585
+ if (this._fluidKey === fluidKey) return;
12451
12586
  const key = new AssetKey("minecraft", `${kind}/${surface.key}`, "models", "fluid", "assets", ".json", this.blockState.key?.root);
12452
12587
  const replacement = await this.scene.addSceneObject(
12453
12588
  { key },
12454
- () => new FluidModelObject2(kind, surface.sample, this.blockState.key, this.options)
12589
+ () => new FluidModelObject2(kind, surface.sample, this.blockState.key, {
12590
+ ...this.options,
12591
+ tints: tint === void 0 ? this.options.tints : { ...this.options.tints, 0: tint }
12592
+ })
12455
12593
  );
12456
12594
  const previous = this._fluidModel;
12457
12595
  const matrix = previous ? this.getModelMatrix(previous) : new Matrix411().makeTranslation(...this.position.toArray());
@@ -12460,7 +12598,7 @@ var _BlockObject = class _BlockObject extends SceneObject {
12460
12598
  this._models = this._models.filter((model) => model !== previous);
12461
12599
  this._models.push(replacement);
12462
12600
  this._fluidModel = replacement;
12463
- this._fluidKey = surface.key;
12601
+ this._fluidKey = fluidKey;
12464
12602
  this._isInstanced || (this._isInstanced = isInstanceReference(replacement));
12465
12603
  this._instanceCounter = this._isInstanced ? 1 : 0;
12466
12604
  if (previous) this.removeModels([previous]);
@@ -12483,6 +12621,7 @@ var _BlockObject = class _BlockObject extends SceneObject {
12483
12621
  }
12484
12622
  async recreateModels() {
12485
12623
  this.clearModels();
12624
+ 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;
12486
12625
  if (resolveBlockFluidState(this.blockState.key, this.state, this._fluidRules)?.renderModel === false) {
12487
12626
  await this.updateFluid(this._fluidSampler);
12488
12627
  return;
@@ -12517,7 +12656,13 @@ var _BlockObject = class _BlockObject extends SceneObject {
12517
12656
  const model = await Models.getMerged(modelKey);
12518
12657
  const options = {
12519
12658
  ...this.options,
12520
- tints: model ? await BlockTints.get(this.blockState.key, this.state, model, this.options.tints) : this.options.tints,
12659
+ tints: model ? await BlockTints.get(
12660
+ this.blockState.key,
12661
+ this.state,
12662
+ model,
12663
+ this.options.tints,
12664
+ () => BlockTints.getBiomeColor(this.blockState.key, this.options.biome, this.biomePosition)
12665
+ ) : this.options.tints,
12521
12666
  uvLockRotation: variant.uvlock && (rotation.x !== 0 || rotation.y !== 0) ? [rotation.x, rotation.y, rotation.z] : void 0,
12522
12667
  cullMask: this.options.displayPosition ? 0 : ModelCulling.toLocalMask(this._cullMask, rotation)
12523
12668
  };
@@ -13104,6 +13249,321 @@ init_ModelObject();
13104
13249
  // src/index.ts
13105
13250
  init_nbt();
13106
13251
 
13252
+ // src/renderer/FlyControls.ts
13253
+ import { Controls, Euler as Euler11, MathUtils as MathUtils2, Quaternion as Quaternion6, Vector3 as Vector313 } from "three";
13254
+ var VANILLA_FLY_SPEED = 10.89;
13255
+ var PITCH_LIMIT = Math.PI / 2;
13256
+ var MOVE_EPSILON = 1e-6;
13257
+ var _euler = new Euler11(0, 0, 0, "YXZ");
13258
+ var _forward = new Vector313();
13259
+ var _right = new Vector313();
13260
+ var _targetVelocity = new Vector313();
13261
+ var FlyControls = class extends Controls {
13262
+ constructor(camera, domElement) {
13263
+ super(camera, domElement ?? null);
13264
+ this.isFlyControls = true;
13265
+ /** Horizontal and vertical speed in scene units per second; vanilla creative flight by default. */
13266
+ this.movementSpeed = 16 * VANILLA_FLY_SPEED;
13267
+ /** Speed multiplier while the sprint key is held. */
13268
+ this.sprintMultiplier = 2;
13269
+ /** Radians per pixel of pointer movement. */
13270
+ this.lookSpeed = 2e-3;
13271
+ /** Smooth acceleration and deceleration, approximating vanilla flight momentum. */
13272
+ this.enableDamping = true;
13273
+ /** Portion of the remaining velocity change applied per 60 Hz frame (0–1) when damping is enabled. */
13274
+ this.dampingFactor = 0.25;
13275
+ /** Capture the pointer on click so the mouse turns the camera without dragging; dragging works regardless. */
13276
+ this.pointerLock = true;
13277
+ /** Scroll the wheel to change `movementSpeed` within `speedRange`. */
13278
+ this.enableWheelSpeed = true;
13279
+ /** Minimum and maximum `movementSpeed` reachable with the wheel. */
13280
+ this.speedRange = [16, 16 * 500];
13281
+ /** Touch joystick travel in CSS pixels for full speed. */
13282
+ this.joystickRadius = 64;
13283
+ /** Key bindings by `KeyboardEvent.code`. */
13284
+ this.keys = {
13285
+ forward: ["KeyW", "ArrowUp"],
13286
+ back: ["KeyS", "ArrowDown"],
13287
+ left: ["KeyA", "ArrowLeft"],
13288
+ right: ["KeyD", "ArrowRight"],
13289
+ up: ["Space"],
13290
+ down: ["ShiftLeft", "ShiftRight"],
13291
+ sprint: ["ControlLeft", "ControlRight"]
13292
+ };
13293
+ this._pressed = /* @__PURE__ */ new Set();
13294
+ this._velocity = new Vector313();
13295
+ this._lastPosition = new Vector313();
13296
+ this._lastQuaternion = new Quaternion6();
13297
+ this._position0 = new Vector313();
13298
+ this._quaternion0 = new Quaternion6();
13299
+ this._dragMoved = false;
13300
+ this._locked = false;
13301
+ this._onKeyDown = (event) => this.handleKey(event, true);
13302
+ this._onKeyUp = (event) => this.handleKey(event, false);
13303
+ this._onPointerDown = (event) => this.handlePointerDown(event);
13304
+ this._onPointerMove = (event) => this.handlePointerMove(event);
13305
+ this._onPointerUp = (event) => this.handlePointerUp(event);
13306
+ this._onPointerLockChange = () => this.handlePointerLockChange();
13307
+ this._onContextMenu = (event) => event.preventDefault();
13308
+ this._onWheel = (event) => this.handleWheel(event);
13309
+ this._onBlur = () => this.releaseKeys();
13310
+ this._lastPosition.copy(camera.position);
13311
+ this._lastQuaternion.copy(camera.quaternion);
13312
+ this.saveState();
13313
+ if (domElement) this.connect(domElement);
13314
+ }
13315
+ connect(element) {
13316
+ super.connect(element);
13317
+ const document2 = element.ownerDocument;
13318
+ document2.addEventListener("keydown", this._onKeyDown);
13319
+ document2.addEventListener("keyup", this._onKeyUp);
13320
+ document2.addEventListener("pointerlockchange", this._onPointerLockChange);
13321
+ document2.defaultView?.addEventListener("blur", this._onBlur);
13322
+ element.addEventListener("pointerdown", this._onPointerDown);
13323
+ element.addEventListener("pointermove", this._onPointerMove);
13324
+ element.addEventListener("pointerup", this._onPointerUp);
13325
+ element.addEventListener("pointercancel", this._onPointerUp);
13326
+ element.addEventListener("contextmenu", this._onContextMenu);
13327
+ element.addEventListener("wheel", this._onWheel, { passive: false });
13328
+ element.addEventListener("blur", this._onBlur);
13329
+ element.style.touchAction = "none";
13330
+ if (element.tabIndex < 0) element.tabIndex = 0;
13331
+ }
13332
+ disconnect() {
13333
+ const element = this.domElement;
13334
+ if (!element) return;
13335
+ const document2 = element.ownerDocument;
13336
+ document2.removeEventListener("keydown", this._onKeyDown);
13337
+ document2.removeEventListener("keyup", this._onKeyUp);
13338
+ document2.removeEventListener("pointerlockchange", this._onPointerLockChange);
13339
+ document2.defaultView?.removeEventListener("blur", this._onBlur);
13340
+ element.removeEventListener("pointerdown", this._onPointerDown);
13341
+ element.removeEventListener("pointermove", this._onPointerMove);
13342
+ element.removeEventListener("pointerup", this._onPointerUp);
13343
+ element.removeEventListener("pointercancel", this._onPointerUp);
13344
+ element.removeEventListener("contextmenu", this._onContextMenu);
13345
+ element.removeEventListener("wheel", this._onWheel);
13346
+ element.removeEventListener("blur", this._onBlur);
13347
+ element.style.touchAction = "auto";
13348
+ this.unlock();
13349
+ this._locked = false;
13350
+ this.releaseKeys();
13351
+ this._dragPointer = void 0;
13352
+ this._joystick = void 0;
13353
+ }
13354
+ dispose() {
13355
+ this.disconnect();
13356
+ }
13357
+ /** Whether mouse look currently owns the pointer. */
13358
+ get locked() {
13359
+ return this._locked;
13360
+ }
13361
+ /** Requests pointer capture; browsers only allow this from a user gesture. */
13362
+ lock() {
13363
+ const element = this.domElement;
13364
+ if (!element || this._locked || typeof element.requestPointerLock !== "function") return;
13365
+ try {
13366
+ const result = element.requestPointerLock();
13367
+ if (result instanceof Promise) result.catch(() => void 0);
13368
+ } catch {
13369
+ }
13370
+ }
13371
+ /** Releases a captured pointer. */
13372
+ unlock() {
13373
+ const document2 = this.domElement?.ownerDocument;
13374
+ if (document2?.pointerLockElement && document2.pointerLockElement === this.domElement) {
13375
+ document2.exitPointerLock();
13376
+ }
13377
+ }
13378
+ /** Stores the camera pose that {@link reset} restores. */
13379
+ saveState() {
13380
+ this._position0.copy(this.object.position);
13381
+ this._quaternion0.copy(this.object.quaternion);
13382
+ }
13383
+ /** Restores the saved camera pose and stops movement. */
13384
+ reset() {
13385
+ this.object.position.copy(this._position0);
13386
+ this.object.quaternion.copy(this._quaternion0);
13387
+ this._velocity.set(0, 0, 0);
13388
+ this.notifyChange();
13389
+ }
13390
+ /** Horizontal yaw in radians, where zero looks toward -Z. */
13391
+ get yaw() {
13392
+ return _euler.setFromQuaternion(this.object.quaternion).y;
13393
+ }
13394
+ /** Vertical pitch in radians, positive looking up. */
13395
+ get pitch() {
13396
+ return _euler.setFromQuaternion(this.object.quaternion).x;
13397
+ }
13398
+ /** Turns the camera; pitch is clamped to straight up and down. */
13399
+ setRotation(yaw, pitch) {
13400
+ _euler.set(MathUtils2.clamp(pitch, -PITCH_LIMIT, PITCH_LIMIT), yaw, 0);
13401
+ this.object.quaternion.setFromEuler(_euler);
13402
+ this.notifyChange();
13403
+ }
13404
+ /** Points the camera at a position without rolling it. */
13405
+ lookAt(target) {
13406
+ _forward.copy(target).sub(this.object.position);
13407
+ if (_forward.lengthSq() < MOVE_EPSILON) return;
13408
+ _forward.normalize();
13409
+ this.setRotation(Math.atan2(-_forward.x, -_forward.z), Math.asin(MathUtils2.clamp(_forward.y, -1, 1)));
13410
+ }
13411
+ /** Whether an action's key is held. */
13412
+ isPressed(action) {
13413
+ return this._pressed.has(action);
13414
+ }
13415
+ /**
13416
+ * Advances movement by `delta` seconds (measured since the previous call when omitted)
13417
+ * and dispatches `change` when the camera moved.
13418
+ */
13419
+ update(delta) {
13420
+ if (delta === void 0) {
13421
+ const now = typeof performance !== "undefined" ? performance.now() : Date.now();
13422
+ delta = this._lastTime === void 0 ? 0 : (now - this._lastTime) / 1e3;
13423
+ this._lastTime = now;
13424
+ }
13425
+ if (!this.enabled) {
13426
+ this._velocity.set(0, 0, 0);
13427
+ return;
13428
+ }
13429
+ if (delta > 0) this.move(delta);
13430
+ this.notifyChange();
13431
+ }
13432
+ move(delta) {
13433
+ const camera = this.object;
13434
+ let forward = (this._pressed.has("forward") ? 1 : 0) - (this._pressed.has("back") ? 1 : 0);
13435
+ let strafe = (this._pressed.has("right") ? 1 : 0) - (this._pressed.has("left") ? 1 : 0);
13436
+ const climb = (this._pressed.has("up") ? 1 : 0) - (this._pressed.has("down") ? 1 : 0);
13437
+ const joystick = this._joystick;
13438
+ if (joystick) {
13439
+ const radius = Math.max(this.joystickRadius, 1);
13440
+ forward = MathUtils2.clamp(forward + (joystick.originY - joystick.y) / radius, -1, 1);
13441
+ strafe = MathUtils2.clamp(strafe + (joystick.x - joystick.originX) / radius, -1, 1);
13442
+ }
13443
+ _euler.setFromQuaternion(camera.quaternion);
13444
+ const yaw = _euler.y;
13445
+ _forward.set(-Math.sin(yaw), 0, -Math.cos(yaw));
13446
+ _right.set(Math.cos(yaw), 0, -Math.sin(yaw));
13447
+ _targetVelocity.set(0, 0, 0);
13448
+ if (forward || strafe || climb) {
13449
+ _targetVelocity.addScaledVector(_forward, forward).addScaledVector(_right, strafe);
13450
+ if (_targetVelocity.lengthSq() > 1) _targetVelocity.normalize();
13451
+ _targetVelocity.y = climb;
13452
+ const speed = this.movementSpeed * (this._pressed.has("sprint") ? this.sprintMultiplier : 1);
13453
+ _targetVelocity.multiplyScalar(speed);
13454
+ }
13455
+ if (this.enableDamping) {
13456
+ const blend = 1 - Math.pow(1 - MathUtils2.clamp(this.dampingFactor, 0, 1), delta * 60);
13457
+ this._velocity.lerp(_targetVelocity, blend);
13458
+ if (_targetVelocity.lengthSq() === 0 && this._velocity.lengthSq() < MOVE_EPSILON) this._velocity.set(0, 0, 0);
13459
+ } else {
13460
+ this._velocity.copy(_targetVelocity);
13461
+ }
13462
+ camera.position.addScaledVector(this._velocity, delta);
13463
+ }
13464
+ notifyChange() {
13465
+ const camera = this.object;
13466
+ if (this._lastPosition.distanceToSquared(camera.position) > MOVE_EPSILON || 8 * (1 - this._lastQuaternion.dot(camera.quaternion)) > MOVE_EPSILON) {
13467
+ this._lastPosition.copy(camera.position);
13468
+ this._lastQuaternion.copy(camera.quaternion);
13469
+ this.dispatchEvent({ type: "change" });
13470
+ }
13471
+ }
13472
+ releaseKeys() {
13473
+ this._pressed.clear();
13474
+ }
13475
+ handleKey(event, down) {
13476
+ const action = this.actionFor(event.code);
13477
+ if (!action) return;
13478
+ const ownsInput = this.enabled && (this._locked || event.target === this.domElement);
13479
+ if (down && !ownsInput) return;
13480
+ const pressed = this._pressed.has(action);
13481
+ if (down) this._pressed.add(action);
13482
+ else this._pressed.delete(action);
13483
+ if (ownsInput || this.enabled && pressed) event.preventDefault();
13484
+ }
13485
+ actionFor(code) {
13486
+ for (const action in this.keys) {
13487
+ if (this.keys[action].includes(code)) return action;
13488
+ }
13489
+ return void 0;
13490
+ }
13491
+ handlePointerDown(event) {
13492
+ const element = this.domElement;
13493
+ if (!this.enabled || !element) return;
13494
+ element.focus?.({ preventScroll: true });
13495
+ if (this._locked) return;
13496
+ if (event.pointerType === "touch" && !this._joystick && this.onJoystickSide(event)) {
13497
+ this._joystick = { id: event.pointerId, originX: event.clientX, originY: event.clientY, x: event.clientX, y: event.clientY };
13498
+ element.setPointerCapture?.(event.pointerId);
13499
+ return;
13500
+ }
13501
+ if (this._dragPointer !== void 0) return;
13502
+ if (event.pointerType === "mouse" && event.button !== 0 && event.button !== 2) return;
13503
+ this._dragPointer = event.pointerId;
13504
+ this._dragMoved = false;
13505
+ element.setPointerCapture?.(event.pointerId);
13506
+ }
13507
+ onJoystickSide(event) {
13508
+ const rect = this.domElement?.getBoundingClientRect?.();
13509
+ return !!rect && event.clientX < rect.left + rect.width / 2;
13510
+ }
13511
+ handlePointerMove(event) {
13512
+ if (!this.enabled) return;
13513
+ const joystick = this._joystick;
13514
+ if (joystick && event.pointerId === joystick.id) {
13515
+ joystick.x = event.clientX;
13516
+ joystick.y = event.clientY;
13517
+ return;
13518
+ }
13519
+ if (!this._locked && event.pointerId !== this._dragPointer) return;
13520
+ const dx = event.movementX ?? 0;
13521
+ const dy = event.movementY ?? 0;
13522
+ if (!dx && !dy) return;
13523
+ this._dragMoved = true;
13524
+ this.turn(dx, dy);
13525
+ }
13526
+ handlePointerUp(event) {
13527
+ if (this._joystick?.id === event.pointerId) {
13528
+ this._joystick = void 0;
13529
+ this.domElement?.releasePointerCapture?.(event.pointerId);
13530
+ return;
13531
+ }
13532
+ if (event.pointerId !== this._dragPointer) return;
13533
+ this._dragPointer = void 0;
13534
+ this.domElement?.releasePointerCapture?.(event.pointerId);
13535
+ if (this.enabled && this.pointerLock && !this._dragMoved && event.pointerType === "mouse" && event.button === 0) {
13536
+ this.lock();
13537
+ }
13538
+ }
13539
+ handleWheel(event) {
13540
+ if (!this.enabled || !this.enableWheelSpeed) return;
13541
+ event.preventDefault();
13542
+ const steps = event.deltaMode === 0 ? event.deltaY / 100 : event.deltaY;
13543
+ const [min, max] = this.speedRange;
13544
+ this.movementSpeed = MathUtils2.clamp(this.movementSpeed * Math.pow(1.15, -steps), Math.min(min, max), Math.max(min, max));
13545
+ }
13546
+ handlePointerLockChange() {
13547
+ const document2 = this.domElement?.ownerDocument;
13548
+ const locked = !!document2 && document2.pointerLockElement === this.domElement;
13549
+ if (locked === this._locked) return;
13550
+ this._locked = locked;
13551
+ if (!locked) this.releaseKeys();
13552
+ this.dispatchEvent({ type: locked ? "lock" : "unlock" });
13553
+ }
13554
+ turn(dx, dy) {
13555
+ _euler.setFromQuaternion(this.object.quaternion);
13556
+ _euler.y -= dx * this.lookSpeed;
13557
+ _euler.x = MathUtils2.clamp(_euler.x - dy * this.lookSpeed, -PITCH_LIMIT, PITCH_LIMIT);
13558
+ _euler.z = 0;
13559
+ this.object.quaternion.setFromEuler(_euler);
13560
+ this.notifyChange();
13561
+ }
13562
+ };
13563
+ function isFlyControls(obj) {
13564
+ return obj?.isFlyControls === true;
13565
+ }
13566
+
13107
13567
  // src/renderer/MineRenderScene.ts
13108
13568
  init_SceneObject();
13109
13569
  var import_ts_deepmerge8 = __toESM(require_dist());
@@ -14170,7 +14630,7 @@ function isMineRenderScene(obj) {
14170
14630
  }
14171
14631
 
14172
14632
  // src/renderer/Renderer.ts
14173
- import { AxesHelper as AxesHelper2, GridHelper, OrthographicCamera, PCFShadowMap, PerspectiveCamera, SRGBColorSpace as SRGBColorSpace7, Vector3 as Vector314, WebGLRenderer } from "three";
14633
+ import { AxesHelper as AxesHelper2, GridHelper, OrthographicCamera, PCFShadowMap, PerspectiveCamera, SRGBColorSpace as SRGBColorSpace7, Vector3 as Vector315, WebGLRenderer } from "three";
14174
14634
  var import_ts_deepmerge9 = __toESM(require_dist());
14175
14635
  init_util();
14176
14636
  init_Model();
@@ -14182,11 +14642,11 @@ import { EffectComposer, RenderPass } from "postprocessing";
14182
14642
  import {
14183
14643
  EventDispatcher,
14184
14644
  MOUSE,
14185
- Quaternion as Quaternion6,
14645
+ Quaternion as Quaternion7,
14186
14646
  Spherical,
14187
14647
  TOUCH,
14188
14648
  Vector2 as Vector26,
14189
- Vector3 as Vector313
14649
+ Vector3 as Vector314
14190
14650
  } from "three";
14191
14651
  var _changeEvent = { type: "change" };
14192
14652
  var _startEvent = { type: "start" };
@@ -14200,7 +14660,7 @@ var OrbitControls = class extends EventDispatcher {
14200
14660
  this.domElement = domElement;
14201
14661
  this.domElement.style.touchAction = "none";
14202
14662
  this.enabled = true;
14203
- this.target = new Vector313();
14663
+ this.target = new Vector314();
14204
14664
  this.minDistance = 0;
14205
14665
  this.maxDistance = Infinity;
14206
14666
  this.minZoom = 0;
@@ -14252,11 +14712,11 @@ var OrbitControls = class extends EventDispatcher {
14252
14712
  state = STATE.NONE;
14253
14713
  };
14254
14714
  this.update = function() {
14255
- const offset = new Vector313();
14256
- const quat = new Quaternion6().setFromUnitVectors(object4.up, new Vector313(0, 1, 0));
14715
+ const offset = new Vector314();
14716
+ const quat = new Quaternion7().setFromUnitVectors(object4.up, new Vector314(0, 1, 0));
14257
14717
  const quatInverse = quat.clone().invert();
14258
- const lastPosition = new Vector313();
14259
- const lastQuaternion = new Quaternion6();
14718
+ const lastPosition = new Vector314();
14719
+ const lastQuaternion = new Quaternion7();
14260
14720
  const twoPI = 2 * Math.PI;
14261
14721
  return function update() {
14262
14722
  const position = scope.object.position;
@@ -14345,7 +14805,7 @@ var OrbitControls = class extends EventDispatcher {
14345
14805
  const spherical = new Spherical();
14346
14806
  const sphericalDelta = new Spherical();
14347
14807
  let scale = 1;
14348
- const panOffset = new Vector313();
14808
+ const panOffset = new Vector314();
14349
14809
  let zoomChanged = false;
14350
14810
  const rotateStart = new Vector26();
14351
14811
  const rotateEnd = new Vector26();
@@ -14371,7 +14831,7 @@ var OrbitControls = class extends EventDispatcher {
14371
14831
  sphericalDelta.phi -= angle;
14372
14832
  }
14373
14833
  const panLeft = function() {
14374
- const v = new Vector313();
14834
+ const v = new Vector314();
14375
14835
  return function panLeft2(distance, objectMatrix) {
14376
14836
  v.setFromMatrixColumn(objectMatrix, 0);
14377
14837
  v.multiplyScalar(-distance);
@@ -14379,7 +14839,7 @@ var OrbitControls = class extends EventDispatcher {
14379
14839
  };
14380
14840
  }();
14381
14841
  const panUp = function() {
14382
- const v = new Vector313();
14842
+ const v = new Vector314();
14383
14843
  return function panUp2(distance, objectMatrix) {
14384
14844
  if (scope.screenSpacePanning === true) {
14385
14845
  v.setFromMatrixColumn(objectMatrix, 1);
@@ -14392,7 +14852,7 @@ var OrbitControls = class extends EventDispatcher {
14392
14852
  };
14393
14853
  }();
14394
14854
  const pan = function() {
14395
- const offset = new Vector313();
14855
+ const offset = new Vector314();
14396
14856
  return function pan2(deltaX, deltaY) {
14397
14857
  const element = scope.domElement;
14398
14858
  if (scope.object.isPerspectiveCamera) {
@@ -14947,20 +15407,77 @@ var _Renderer = class _Renderer {
14947
15407
  }
14948
15408
  createControls() {
14949
15409
  if (!this.options.controls?.enabled) return void 0;
14950
- const position = this.camera.position.clone();
14951
- const controls = new OrbitControls(this.camera, this.renderer.domElement);
14952
- this.camera.position.copy(position);
15410
+ return this.createControlsFor(this.options.controls.mode ?? "orbit", this.initialTarget());
15411
+ }
15412
+ initialTarget() {
14953
15413
  const target = this.options.camera.lookingAt;
14954
- if (isVector3(target)) {
15414
+ if (isVector3(target)) return target.clone();
15415
+ if (isTripleArray(target)) return new Vector315(target[0], target[1], target[2]);
15416
+ return new Vector315();
15417
+ }
15418
+ createControlsFor(mode, target) {
15419
+ let controls;
15420
+ if (mode === "fly") {
15421
+ controls = new FlyControls(this.camera, this.renderer.domElement);
15422
+ controls.lookAt(target);
15423
+ controls.saveState();
15424
+ } else {
15425
+ const position = this.camera.position.clone();
15426
+ controls = new OrbitControls(this.camera, this.renderer.domElement);
15427
+ this.camera.position.copy(position);
14955
15428
  controls.target.copy(target);
14956
- } else if (isTripleArray(target)) {
14957
- controls.target.set(target[0], target[1], target[2]);
15429
+ controls.update();
15430
+ controls.saveState();
14958
15431
  }
14959
- controls.update();
14960
- controls.saveState();
15432
+ this._controlsTime = void 0;
14961
15433
  this.registerEventDispatcher(controls);
14962
15434
  return controls;
14963
15435
  }
15436
+ /**
15437
+ * Replaces renderer-owned controls with the selected mode, keeping the camera where it is.
15438
+ * Switching to orbit controls targets the point the camera looks at, at the previous orbit distance.
15439
+ * Returns the new controls, or undefined when controls were disabled at construction or after disposal.
15440
+ */
15441
+ setControlsMode(mode) {
15442
+ const current = this._controls;
15443
+ if (this._disposed || !current) return void 0;
15444
+ if (this.controlsMode === mode) return current;
15445
+ let target;
15446
+ if (isFlyControls(current)) {
15447
+ const distance = this._orbitDistance ?? (this.camera.position.distanceTo(this.initialTarget()) || 1);
15448
+ target = new Vector315(0, 0, -distance).applyQuaternion(this.camera.quaternion).add(this.camera.position);
15449
+ } else {
15450
+ target = current.target.clone();
15451
+ this._orbitDistance = this.camera.position.distanceTo(target);
15452
+ }
15453
+ const enabled = current.enabled;
15454
+ this.disposeControls(current);
15455
+ this._controls = this.createControlsFor(mode, target);
15456
+ this._controls.enabled = enabled;
15457
+ this.dirty = true;
15458
+ return this._controls;
15459
+ }
15460
+ disposeControls(controls) {
15461
+ this._controls = void 0;
15462
+ this.unregisterEventDispatcher(controls);
15463
+ controls.enabled = false;
15464
+ controls.dispose();
15465
+ }
15466
+ /** Advances owned controls; without a frame time, flight movement does not progress. */
15467
+ updateControls(time) {
15468
+ const controls = this._controls;
15469
+ if (!controls?.enabled) {
15470
+ this._controlsTime = void 0;
15471
+ return;
15472
+ }
15473
+ if (!isFlyControls(controls)) {
15474
+ controls.update();
15475
+ return;
15476
+ }
15477
+ const previous = this._controlsTime;
15478
+ if (time !== void 0) this._controlsTime = time;
15479
+ controls.update(time === void 0 || previous === void 0 ? 0 : (time - previous) / 1e3);
15480
+ }
14964
15481
  //</editor-fold>
14965
15482
  init() {
14966
15483
  if (this._disposed) return;
@@ -15037,6 +15554,12 @@ var _Renderer = class _Renderer {
15037
15554
  events.add(changeEvent2);
15038
15555
  dispatcher.addEventListener(changeEvent2, this._changeListener);
15039
15556
  }
15557
+ unregisterEventDispatcher(dispatcher) {
15558
+ const events = this._eventDispatchers.get(dispatcher);
15559
+ if (!events) return;
15560
+ for (const event of events) dispatcher.removeEventListener(event, this._changeListener);
15561
+ this._eventDispatchers.delete(dispatcher);
15562
+ }
15040
15563
  /** Updates the camera and canvas size. Width and height are in CSS pixels. */
15041
15564
  resize(width, height) {
15042
15565
  if (this._disposed) return;
@@ -15113,6 +15636,7 @@ var _Renderer = class _Renderer {
15113
15636
  this.renderer.setAnimationLoop(null);
15114
15637
  }
15115
15638
  this._nextFrameTime = void 0;
15639
+ this._controlsTime = void 0;
15116
15640
  for (const subscription of this._frameCallbacks.values()) subscription.previous = void 0;
15117
15641
  }
15118
15642
  /**
@@ -15142,10 +15666,7 @@ var _Renderer = class _Renderer {
15142
15666
  }
15143
15667
  this._element = void 0;
15144
15668
  if (this._controls) {
15145
- const controls = this._controls;
15146
- this._controls = void 0;
15147
- controls.enabled = false;
15148
- controls.dispose();
15669
+ this.disposeControls(this._controls);
15149
15670
  }
15150
15671
  this.scene.clear();
15151
15672
  for (const helper of this._debugHelpers) {
@@ -15162,9 +15683,7 @@ var _Renderer = class _Renderer {
15162
15683
  }
15163
15684
  animate(t = performance.now()) {
15164
15685
  if (!this._running) return;
15165
- if (this._controls?.enabled) {
15166
- this._controls.update();
15167
- }
15686
+ this.updateControls(t);
15168
15687
  if (!this._running) return;
15169
15688
  if (!this.dirty && !this.options.render.renderAlways && !this._frameCallbacks.size && !this._videoExporter) return;
15170
15689
  const interval = this._frameInterval;
@@ -15201,7 +15720,7 @@ var _Renderer = class _Renderer {
15201
15720
  /** Draws a fresh frame without starting the animation loop or invoking frame callbacks. */
15202
15721
  renderOnce() {
15203
15722
  if (this._disposed) throw new Error("Cannot render a disposed renderer");
15204
- if (this._controls?.enabled) this._controls.update();
15723
+ this.updateControls();
15205
15724
  this.drawFrame();
15206
15725
  }
15207
15726
  /**
@@ -15231,7 +15750,7 @@ var _Renderer = class _Renderer {
15231
15750
  try {
15232
15751
  if (!wasRunning) this.start();
15233
15752
  this._videoExporter = exporter;
15234
- if (this._controls?.enabled) this._controls.update();
15753
+ this.updateControls();
15235
15754
  this.drawFrame();
15236
15755
  } catch (error) {
15237
15756
  exporter.cancel(error);
@@ -15282,6 +15801,19 @@ var _Renderer = class _Renderer {
15282
15801
  get controls() {
15283
15802
  return this._controls;
15284
15803
  }
15804
+ /** Renderer-owned OrbitControls, or undefined in fly mode or without controls. */
15805
+ get orbitControls() {
15806
+ return this._controls && !isFlyControls(this._controls) ? this._controls : void 0;
15807
+ }
15808
+ /** Renderer-owned {@link FlyControls}, or undefined in orbit mode or without controls. */
15809
+ get flyControls() {
15810
+ return isFlyControls(this._controls) ? this._controls : void 0;
15811
+ }
15812
+ /** The active renderer-owned controls mode, or undefined without controls. */
15813
+ get controlsMode() {
15814
+ if (!this._controls) return void 0;
15815
+ return isFlyControls(this._controls) ? "fly" : "orbit";
15816
+ }
15285
15817
  ///
15286
15818
  };
15287
15819
  _Renderer.DEFAULT_OPTIONS = (0, import_ts_deepmerge9.default)({}, {
@@ -15299,8 +15831,8 @@ _Renderer.DEFAULT_OPTIONS = (0, import_ts_deepmerge9.default)({}, {
15299
15831
  top: void 0,
15300
15832
  bottom: void 0
15301
15833
  },
15302
- position: new Vector314(50, 50, 50),
15303
- lookingAt: new Vector314(0, 0, 0)
15834
+ position: new Vector315(50, 50, 50),
15835
+ lookingAt: new Vector315(0, 0, 0)
15304
15836
  },
15305
15837
  render: {
15306
15838
  fpsLimit: 60,
@@ -15315,7 +15847,8 @@ _Renderer.DEFAULT_OPTIONS = (0, import_ts_deepmerge9.default)({}, {
15315
15847
  enabled: true
15316
15848
  },
15317
15849
  controls: {
15318
- enabled: false
15850
+ enabled: false,
15851
+ mode: "orbit"
15319
15852
  },
15320
15853
  debug: {
15321
15854
  grid: false,
@@ -16548,14 +17081,14 @@ var AnvilWorldSource = class {
16548
17081
  };
16549
17082
 
16550
17083
  // src/world/Chunk.ts
16551
- import { Vector3 as Vector317 } from "three";
17084
+ import { Color as Color13, Vector3 as Vector318 } from "three";
16552
17085
  init_BlockStates();
16553
17086
  init_AssetKey();
16554
17087
 
16555
17088
  // src/world/MineRenderWorld.ts
16556
17089
  init_Model();
16557
17090
  init_util();
16558
- import { Vector3 as Vector316 } from "three";
17091
+ import { Vector3 as Vector317 } from "three";
16559
17092
  init_AssetKey();
16560
17093
  init_BlockStates();
16561
17094
  init_CubeFace();
@@ -16564,7 +17097,7 @@ init_CubeFace();
16564
17097
  init_AssetKey();
16565
17098
  init_Models();
16566
17099
  init_CubeFace();
16567
- import { BufferAttribute as BufferAttribute7, BufferGeometry as BufferGeometry8, Matrix4 as Matrix412, Vector3 as Vector315 } from "three";
17100
+ import { BufferAttribute as BufferAttribute7, BufferGeometry as BufferGeometry8, Matrix4 as Matrix412, Vector3 as Vector316 } from "three";
16568
17101
  init_ModelCulling();
16569
17102
  init_ModelObject();
16570
17103
  init_UVMapper();
@@ -16618,24 +17151,48 @@ var SectionModels = class {
16618
17151
  if (!Number.isInteger(maxAtlasSize) || maxAtlasSize < 1) throw new RangeError("maxAtlasSize must be a positive integer");
16619
17152
  }
16620
17153
  /** Returns templates for the matched parts, or `undefined` when the block needs an individual render object. */
16621
- async get(blockState3, properties = {}, position) {
16622
- const variantPosition = position ? [...position] : void 0;
16623
- return (await this.prepareState(blockState3, properties))?.pick(variantPosition);
17154
+ async get(blockState3, properties = {}, position, biome) {
17155
+ const biomePosition = position ? [...position] : void 0;
17156
+ const prepared = await this.state(blockState3, properties);
17157
+ if (!prepared) return void 0;
17158
+ return Promise.all(prepared.groups.map(async (group) => {
17159
+ const variant = BlockStateResolver.choose(group, biomePosition);
17160
+ const choice = prepared.choices.get(variant);
17161
+ const tints = await BlockTints.get(
17162
+ blockState3.key,
17163
+ prepared.state,
17164
+ choice.model,
17165
+ void 0,
17166
+ () => BlockTints.getBiomeColor(blockState3.key, biome, biomePosition)
17167
+ );
17168
+ return this.template(variant, choice, tints);
17169
+ }));
17170
+ }
17171
+ /** Resolves section templates for one tint palette, with a synchronous weighted choice for each cell. */
17172
+ async prepareState(blockState3, properties = {}, biomeColor) {
17173
+ const prepared = await this.state(blockState3, properties);
17174
+ if (!prepared) return void 0;
17175
+ let color2;
17176
+ const resolveColor = typeof biomeColor === "function" ? () => color2 ?? (color2 = biomeColor()) : biomeColor;
17177
+ const templates = /* @__PURE__ */ new Map();
17178
+ await Promise.all([...prepared.choices].map(async ([variant, choice]) => {
17179
+ const tints = await BlockTints.get(blockState3.key, prepared.state, choice.model, void 0, resolveColor);
17180
+ templates.set(variant, await this.template(variant, choice, tints));
17181
+ }));
17182
+ return { pick: (position) => prepared.groups.map((group) => templates.get(BlockStateResolver.choose(group, position))) };
16624
17183
  }
16625
- /** Resolves section templates once, with a synchronous weighted choice for each cell. */
16626
- async prepareState(blockState3, properties = {}) {
17184
+ async state(blockState3, properties) {
16627
17185
  let states = this.states.get(blockState3);
16628
17186
  if (!states) this.states.set(blockState3, states = /* @__PURE__ */ new Map());
16629
17187
  const key = JSON.stringify(Object.entries(properties).sort(([a], [b]) => a.localeCompare(b)));
16630
- const prepared = await cached(states, key, () => this.prepare(blockState3, properties));
16631
- if (!prepared) return void 0;
16632
- return { pick: (position) => prepared.groups.map((group) => prepared.templates.get(BlockStateResolver.choose(group, position))) };
17188
+ return cached(states, key, () => this.prepare(blockState3, properties));
16633
17189
  }
16634
17190
  /** Returns the shared still/flow atlas used by per-block fluid objects. */
16635
17191
  fluidAtlas(kind, root) {
16636
- return cached(this.fluidAtlases, kind, async () => {
17192
+ const key = new AssetKey("minecraft", kind, "models", "fluid", "assets", ".json", root);
17193
+ return cached(this.fluidAtlases, key.serialize(), async () => {
16637
17194
  const atlas = await UVMapper.getAtlas({
16638
- key: new AssetKey("minecraft", kind, "models", "fluid", "assets", ".json", root),
17195
+ key,
16639
17196
  textures: { still: `minecraft:block/${kind}_still`, flow: `minecraft:block/${kind}_flow` },
16640
17197
  elements: []
16641
17198
  });
@@ -16647,64 +17204,67 @@ var SectionModels = class {
16647
17204
  const state = { ...await BlockStateResolver.defaults(blockState3), ...properties };
16648
17205
  const groups = BlockStateResolver.matching(blockState3, state);
16649
17206
  if (!groups.length) return void 0;
16650
- const templates = /* @__PURE__ */ new Map();
17207
+ const prepared = /* @__PURE__ */ new Map();
16651
17208
  for (const group of groups) {
16652
17209
  const choices = Array.isArray(group) ? group : [group];
16653
17210
  if (!choices.length) return void 0;
16654
17211
  for (const variant of choices) {
16655
17212
  if (!variant.model) return void 0;
16656
- const model = await Models.getMerged(AssetKey.parse("models", variant.model));
17213
+ const modelKey = AssetKey.parse("models", variant.model);
17214
+ modelKey.root = blockState3.key?.root;
17215
+ const model = await Models.getMerged(modelKey);
16657
17216
  if (!model?.elements?.length) return void 0;
16658
17217
  const atlas = await UVMapper.getAtlas(model);
16659
17218
  const rotation = BlockStateResolver.rotation(variant);
16660
17219
  if (!atlas || atlas.image.width > this.maxAtlasSize || atlas.image.height > this.maxAtlasSize) return void 0;
16661
- const tints = await BlockTints.get(blockState3.key, state, model);
16662
- let models = this.models.get(model);
16663
- if (!models) this.models.set(model, models = /* @__PURE__ */ new Map());
16664
- const key = JSON.stringify([rotation.x, rotation.y, rotation.z, variant.uvlock, tints]);
16665
- const template = await cached(models, key, async () => {
16666
- const object4 = new SectionModel(model, {
16667
- instanceMeshes: false,
16668
- mergeMeshes: true,
16669
- tints,
16670
- uvLockRotation: variant.uvlock && (rotation.x !== 0 || rotation.y !== 0) ? [rotation.x, rotation.y, rotation.z] : void 0
16671
- });
16672
- await object4.init();
16673
- let geometry = object4.children[0].geometry;
16674
- geometry.applyMatrix4(new Matrix412().makeRotationFromEuler(rotation));
16675
- object4.clear();
16676
- const keep = [];
16677
- const cullFaces = [];
16678
- for (const element of atlas.model.elements) {
16679
- for (const face of CUBE_FACES) {
16680
- const data = element.faces[face];
16681
- keep.push(data !== void 0);
16682
- if (!data) continue;
16683
- const local = CUBE_FACES.findIndex((name) => name === data.cullface);
16684
- if (local < 0) {
16685
- cullFaces.push(0);
16686
- continue;
16687
- }
16688
- const normal = new Vector315(...CUBE_FACE_OFFSETS[local]).applyEuler(rotation);
16689
- 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);
16690
- cullFaces.push(world < 0 ? 0 : 1 << world);
16691
- }
16692
- }
16693
- geometry = compactQuads(geometry, keep);
16694
- const template2 = {
16695
- geometry,
16696
- atlas,
16697
- cullFaces: new Uint8Array(cullFaces),
16698
- occludes: ModelCulling.isOpaqueFullCube(atlas) && ModelCulling.toLocalMask(63, rotation) === 63,
16699
- layer: atlas.hasTranslucency ? 1 : 0
16700
- };
16701
- this.templates.add(template2);
16702
- return template2;
16703
- });
16704
- templates.set(variant, template);
17220
+ prepared.set(variant, { model, atlas, rotation });
16705
17221
  }
16706
17222
  }
16707
- return { groups, templates };
17223
+ return { state, groups, choices: prepared };
17224
+ }
17225
+ async template(variant, { model, atlas, rotation }, tints) {
17226
+ let models = this.models.get(model);
17227
+ if (!models) this.models.set(model, models = /* @__PURE__ */ new Map());
17228
+ const key = JSON.stringify([rotation.x, rotation.y, rotation.z, variant.uvlock, tints]);
17229
+ return cached(models, key, async () => {
17230
+ const object4 = new SectionModel(model, {
17231
+ instanceMeshes: false,
17232
+ mergeMeshes: true,
17233
+ tints,
17234
+ uvLockRotation: variant.uvlock && (rotation.x !== 0 || rotation.y !== 0) ? [rotation.x, rotation.y, rotation.z] : void 0
17235
+ });
17236
+ await object4.init();
17237
+ let geometry = object4.children[0].geometry;
17238
+ geometry.applyMatrix4(new Matrix412().makeRotationFromEuler(rotation));
17239
+ object4.clear();
17240
+ const keep = [];
17241
+ const cullFaces = [];
17242
+ for (const element of atlas.model.elements) {
17243
+ for (const face of CUBE_FACES) {
17244
+ const data = element.faces[face];
17245
+ keep.push(data !== void 0);
17246
+ if (!data) continue;
17247
+ const local = CUBE_FACES.findIndex((name) => name === data.cullface);
17248
+ if (local < 0) {
17249
+ cullFaces.push(0);
17250
+ continue;
17251
+ }
17252
+ const normal = new Vector316(...CUBE_FACE_OFFSETS[local]).applyEuler(rotation);
17253
+ 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);
17254
+ cullFaces.push(world < 0 ? 0 : 1 << world);
17255
+ }
17256
+ }
17257
+ geometry = compactQuads(geometry, keep);
17258
+ const template = {
17259
+ geometry,
17260
+ atlas,
17261
+ cullFaces: new Uint8Array(cullFaces),
17262
+ occludes: ModelCulling.isOpaqueFullCube(atlas) && ModelCulling.toLocalMask(63, rotation) === 63,
17263
+ layer: atlas.hasTranslucency ? 1 : 0
17264
+ };
17265
+ this.templates.add(template);
17266
+ return template;
17267
+ });
16708
17268
  }
16709
17269
  /** Disposes prepared geometry and clears this world's template caches. */
16710
17270
  clear() {
@@ -16881,17 +17441,17 @@ var MineRenderWorld = class {
16881
17441
  }
16882
17442
  getBlockAt(posOrX, y, z) {
16883
17443
  if (typeof posOrX == "number") {
16884
- return this.getBlockAt(new Vector316(posOrX, y, z));
17444
+ return this.getBlockAt(new Vector317(posOrX, y, z));
16885
17445
  }
16886
17446
  if (isTripleArray(posOrX)) {
16887
- return this.getBlockAt(new Vector316(posOrX[0], posOrX[1], posOrX[2]));
17447
+ return this.getBlockAt(new Vector317(posOrX[0], posOrX[1], posOrX[2]));
16888
17448
  }
16889
17449
  this.validatePosBounds(posOrX);
16890
17450
  return this.getChunkAt(posOrX)?.getBlockAt(posOrX);
16891
17451
  }
16892
17452
  getBiomeAt(posOrX, y, z) {
16893
- if (typeof posOrX === "number") return this.getBiomeAt(new Vector316(posOrX, y, z));
16894
- if (isTripleArray(posOrX)) return this.getBiomeAt(new Vector316(...posOrX));
17453
+ if (typeof posOrX === "number") return this.getBiomeAt(new Vector317(posOrX, y, z));
17454
+ if (isTripleArray(posOrX)) return this.getBiomeAt(new Vector317(...posOrX));
16895
17455
  this.validatePosBounds(posOrX);
16896
17456
  const cx = Math.floor(posOrX.x / 16), cy = Math.floor(posOrX.y / 16), cz = Math.floor(posOrX.z / 16);
16897
17457
  const lx = posOrX.x - cx * 16, ly = posOrX.y - cy * 16, lz = posOrX.z - cz * 16;
@@ -16899,10 +17459,10 @@ var MineRenderWorld = class {
16899
17459
  }
16900
17460
  async setBlockAt(posOrX, yOrBlock, z, block) {
16901
17461
  if (typeof posOrX == "number") {
16902
- return this.setBlockAt(new Vector316(posOrX, yOrBlock, z), block);
17462
+ return this.setBlockAt(new Vector317(posOrX, yOrBlock, z), block);
16903
17463
  }
16904
17464
  if (isTripleArray(posOrX)) {
16905
- return this.setBlockAt(new Vector316(posOrX[0], posOrX[1], posOrX[2]), yOrBlock);
17465
+ return this.setBlockAt(new Vector317(posOrX[0], posOrX[1], posOrX[2]), yOrBlock);
16906
17466
  }
16907
17467
  return this.placeBlock(posOrX, yOrBlock);
16908
17468
  }
@@ -16913,10 +17473,10 @@ var MineRenderWorld = class {
16913
17473
  }
16914
17474
  async setBlockVisibleAt(posOrX, yOrVisible, z, visible) {
16915
17475
  if (typeof posOrX === "number") {
16916
- return this.setBlockVisibleAt(new Vector316(posOrX, yOrVisible, z), visible);
17476
+ return this.setBlockVisibleAt(new Vector317(posOrX, yOrVisible, z), visible);
16917
17477
  }
16918
17478
  if (isTripleArray(posOrX)) {
16919
- return this.setBlockVisibleAt(new Vector316(...posOrX), yOrVisible);
17479
+ return this.setBlockVisibleAt(new Vector317(...posOrX), yOrVisible);
16920
17480
  }
16921
17481
  this.validatePosBounds(posOrX);
16922
17482
  await this.getChunkAt(posOrX)?.setBlockVisibleAt(posOrX, yOrVisible);
@@ -16942,7 +17502,7 @@ var MineRenderWorld = class {
16942
17502
  const collect = async (positions2) => {
16943
17503
  for (const pos of positions2) changes.set(pos.toArray().join(","), pos);
16944
17504
  };
16945
- const place = (block) => this.placeBlock(new Vector316(...block.position), block, collect);
17505
+ const place = (block) => this.placeBlock(new Vector317(...block.position), block, collect);
16946
17506
  const keys = /* @__PURE__ */ new Map();
16947
17507
  const types = /* @__PURE__ */ new Set();
16948
17508
  for (const block of multiblock.blocks) {
@@ -16958,7 +17518,7 @@ var MineRenderWorld = class {
16958
17518
  }
16959
17519
  if (!executor) {
16960
17520
  const groups2 = /* @__PURE__ */ new Map();
16961
- const position = new Vector316();
17521
+ const position = new Vector317();
16962
17522
  for (const block of multiblock.blocks) {
16963
17523
  const [x, y, z] = block.position;
16964
17524
  this.validatePosBounds(position.set(x, y, z));
@@ -16998,7 +17558,7 @@ var MineRenderWorld = class {
16998
17558
  try {
16999
17559
  positions = await chunk.placeBlocks(blocks);
17000
17560
  } finally {
17001
- positions ?? (positions = blocks.map(({ index }) => new Vector316(
17561
+ positions ?? (positions = blocks.map(({ index }) => new Vector317(
17002
17562
  chunk.x * 16 + index % 16,
17003
17563
  chunk.y * 16 + Math.floor(index / 256),
17004
17564
  chunk.z * 16 + Math.floor(index / 16) % 16
@@ -17034,10 +17594,9 @@ var MineRenderWorld = class {
17034
17594
  }
17035
17595
  try {
17036
17596
  await this.clearChunkColumn(chunk.x, chunk.z);
17037
- let sliceStart = performance.now();
17038
17597
  for (const section of chunk.sections) {
17039
17598
  if (section.biomes) {
17040
- const position = new Vector316(chunk.x * 16, section.y * 16, chunk.z * 16);
17599
+ const position = new Vector317(chunk.x * 16, section.y * 16, chunk.z * 16);
17041
17600
  this.validatePosBounds(position);
17042
17601
  const copy = [...section.biomes];
17043
17602
  if (copy.length !== 64 || !copy.every((id) => typeof id === "string" && id.length > 0)) {
@@ -17048,9 +17607,12 @@ var MineRenderWorld = class {
17048
17607
  if (!column) this.biomes.set(key, column = /* @__PURE__ */ new Map());
17049
17608
  column.set(section.y, copy);
17050
17609
  }
17610
+ }
17611
+ let sliceStart = performance.now();
17612
+ for (const section of chunk.sections) {
17051
17613
  if (!executor) {
17052
17614
  if (!occupiedSections.has(section.y)) continue;
17053
- const position = new Vector316(chunk.x * 16, section.y * 16, chunk.z * 16);
17615
+ const position = new Vector317(chunk.x * 16, section.y * 16, chunk.z * 16);
17054
17616
  this.validatePosBounds(position);
17055
17617
  const target = this.getOrCreateChunkAt(position);
17056
17618
  try {
@@ -17144,7 +17706,7 @@ var MineRenderWorld = class {
17144
17706
  this.sectionModels?.clear();
17145
17707
  }
17146
17708
  updateCulling(positions) {
17147
- const neighbor = new Vector316();
17709
+ const neighbor = new Vector317();
17148
17710
  for (const pos of positions) {
17149
17711
  const cx = Math.floor(pos.x / 16), cy = Math.floor(pos.y / 16), cz = Math.floor(pos.z / 16);
17150
17712
  const chunk = this._chunks.get(`${cx}_${cy}_${cz}`);
@@ -17212,18 +17774,34 @@ var MineRenderWorld = class {
17212
17774
  const key = this.worldPosToChunkKey(pos);
17213
17775
  let chunk = this._chunks.get(key);
17214
17776
  if (typeof chunk === "undefined") {
17777
+ const x = Math.floor(pos.x / 16), y = Math.floor(pos.y / 16), z = Math.floor(pos.z / 16);
17215
17778
  chunk = new Chunk(
17216
17779
  this.scene,
17217
- Math.floor(pos.x / 16),
17218
- Math.floor(pos.y / 16),
17219
- Math.floor(pos.z / 16),
17780
+ x,
17781
+ y,
17782
+ z,
17220
17783
  (positions) => this.updateCulling(positions),
17221
17784
  this.sectionModels,
17222
- (x, y, z) => {
17223
- const cx = Math.floor(x / 16), cy = Math.floor(y / 16), cz = Math.floor(z / 16);
17785
+ (x2, y2, z2) => {
17786
+ const cx = Math.floor(x2 / 16), cy = Math.floor(y2 / 16), cz = Math.floor(z2 / 16);
17224
17787
  const section = this._chunks.get(`${cx}_${cy}_${cz}`);
17225
- const index = (y - cy * 16) * 256 + (z - cz * 16) * 16 + x - cx * 16;
17788
+ const index = (y2 - cy * 16) * 256 + (z2 - cz * 16) * 16 + x2 - cx * 16;
17226
17789
  return (section?.fluidByteIndex(index) ?? 0) | (section?.isOccludingIndex(index) ? 64 : 0);
17790
+ },
17791
+ (x2, y2, z2) => this.getBiomeAt(x2, y2, z2),
17792
+ async (biomes) => {
17793
+ if (this._chunks.get(key) !== chunk) return;
17794
+ const columnKey = `${x}_${z}`;
17795
+ let column = this.biomes.get(columnKey);
17796
+ if (biomes) {
17797
+ if (!column) this.biomes.set(columnKey, column = /* @__PURE__ */ new Map());
17798
+ column.set(y, biomes);
17799
+ } else {
17800
+ column?.delete(y);
17801
+ if (!column?.size) this.biomes.delete(columnKey);
17802
+ }
17803
+ await chunk.refreshBiomes();
17804
+ await this._chunks.get(`${x}_${y + 1}_${z}`)?.refreshBiomes();
17227
17805
  }
17228
17806
  );
17229
17807
  this._chunks.set(key, chunk);
@@ -17236,7 +17814,7 @@ var MineRenderWorld = class {
17236
17814
  }
17237
17815
  /** Converts block coordinates to scene units by multiplying each component by 16. */
17238
17816
  static worldToScenePosition(pos) {
17239
- return new Vector316(
17817
+ return new Vector317(
17240
17818
  pos.x * 16,
17241
17819
  pos.y * 16,
17242
17820
  pos.z * 16
@@ -17244,7 +17822,7 @@ var MineRenderWorld = class {
17244
17822
  }
17245
17823
  /** Converts scene units to block coordinates without rounding to an integer block. */
17246
17824
  static sceneToWorldPosition(pos) {
17247
- return new Vector316(
17825
+ return new Vector317(
17248
17826
  pos.x / 16,
17249
17827
  pos.y / 16,
17250
17828
  pos.z / 16
@@ -17278,6 +17856,7 @@ function buildSectionGeometry(input) {
17278
17856
  if (!Number.isInteger(maxAtlasSize) || maxAtlasSize < 1) throw new RangeError("Section atlas size must be a positive integer");
17279
17857
  if (count > indices.length || count > templates.length || count > cullMasks.length) throw new RangeError("Section entry count exceeds its arrays");
17280
17858
  if (fluids && fluids.cells.length !== 5832) throw new RangeError("Section fluid cells must cover an 18\xD718\xD718 neighborhood");
17859
+ if (fluids?.waterTints && fluids.waterTints.length !== 4096 * 3) throw new RangeError("Section water tints must contain RGB for 4096 cells");
17281
17860
  for (const template of templateData) {
17282
17861
  const { quads } = template;
17283
17862
  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) {
@@ -17355,7 +17934,9 @@ function buildSectionGeometry(input) {
17355
17934
  height
17356
17935
  });
17357
17936
  }
17358
- fluidPages.get(fluid).fluids.push({ quads, x, y, z, data });
17937
+ const tintIndex = (x + z * 16 + y * 256) * 3;
17938
+ const tint = fluid === "water" && fluids.waterTints ? fluids.waterTints.subarray(tintIndex, tintIndex + 3) : data.tint;
17939
+ fluidPages.get(fluid).fluids.push({ quads, x, y, z, data, tint });
17359
17940
  }
17360
17941
  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));
17361
17942
  }
@@ -17404,7 +17985,7 @@ function buildSectionGeometry(input) {
17404
17985
  for (let corner = 0; corner < 6; corner++) outputIndices[indexOffset++] = start + template.indices[quad * 6 + corner];
17405
17986
  }
17406
17987
  }
17407
- for (const { quads, x, y, z, data } of page.fluids) {
17988
+ for (const { quads, x, y, z, data, tint } of page.fluids) {
17408
17989
  const atlas = atlases[data.atlas];
17409
17990
  const placement = page.placements[0];
17410
17991
  const mapU = (u) => (placement.x + u * atlas.width) / page.width;
@@ -17428,7 +18009,7 @@ function buildSectionGeometry(input) {
17428
18009
  mapU((rect[0] + rect[2]) / atlas.width),
17429
18010
  mapV(1 - rect[1] / atlas.height)
17430
18011
  ], vertex * 4);
17431
- colors.set(data.tint, vertex * 3);
18012
+ colors.set(tint, vertex * 3);
17432
18013
  }
17433
18014
  outputIndices.set([start, start + 1, start + 2, start, start + 2, start + 3], indexOffset);
17434
18015
  indexOffset += 6;
@@ -17569,7 +18150,7 @@ function toInput(entries, maxAtlasSize, fluids) {
17569
18150
  input.cullMasks[i] = cullMask;
17570
18151
  }
17571
18152
  if (fluids) {
17572
- input.fluids = { cells: fluids.cells };
18153
+ input.fluids = { cells: fluids.cells, waterTints: fluids.waterTints };
17573
18154
  for (const kind of ["water", "lava"]) {
17574
18155
  const atlas = fluids[kind];
17575
18156
  if (!atlas) continue;
@@ -17670,17 +18251,20 @@ var SectionMesh = class _SectionMesh extends Group3 {
17670
18251
  init_FluidGeometry();
17671
18252
  init_FluidQuads();
17672
18253
  init_CubeFace();
18254
+ var WATER_TINT2 = new Color13(4159204).toArray();
17673
18255
  var Chunk = class _Chunk {
17674
- constructor(scene, x, y, z, onBlocksChanged, sectionModels, neighborCell) {
18256
+ constructor(scene, x, y, z, onBlocksChanged, sectionModels, neighborCell, biomeAt, onBiomesChanged) {
17675
18257
  this.onBlocksChanged = onBlocksChanged;
17676
18258
  this.sectionModels = sectionModels;
17677
18259
  this.neighborCell = neighborCell;
18260
+ this.biomeAt = biomeAt;
18261
+ this.onBiomesChanged = onBiomesChanged;
17678
18262
  this.data = new ChunkData();
17679
18263
  this.renderedBlocks = /* @__PURE__ */ new Map();
17680
18264
  this.sectionBlocks = /* @__PURE__ */ new Map();
17681
18265
  this.hiddenBlocks = /* @__PURE__ */ new Set();
17682
18266
  this.fluidCells = new Uint8Array(4096);
17683
- this.blockPosition = new Vector317();
18267
+ this.blockPosition = new Vector318();
17684
18268
  this.meshDirty = false;
17685
18269
  this.meshGeneration = 0;
17686
18270
  this.scene = scene;
@@ -17690,21 +18274,71 @@ var Chunk = class _Chunk {
17690
18274
  }
17691
18275
  getBlockAt(posOrX, y, z) {
17692
18276
  if (typeof posOrX == "number") {
17693
- return this.getBlockAt(new Vector317(posOrX, y, z));
18277
+ return this.getBlockAt(new Vector318(posOrX, y, z));
17694
18278
  }
17695
18279
  if (isTripleArray(posOrX)) {
17696
- return this.getBlockAt(new Vector317(posOrX[0], posOrX[1], posOrX[2]));
18280
+ return this.getBlockAt(new Vector318(posOrX[0], posOrX[1], posOrX[2]));
17697
18281
  }
17698
18282
  const pos = this.blockPosition.set(posOrX.x - this.x * 16, posOrX.y - this.y * 16, posOrX.z - this.z * 16);
17699
18283
  const index = _Chunk.chunkPosToBlockIndex(pos);
17700
18284
  return this.renderedBlocks.get(index);
17701
18285
  }
18286
+ getBiomeAt(posOrX, y, z) {
18287
+ if (typeof posOrX === "number") return this.getBiomeAt(new Vector318(posOrX, y, z));
18288
+ if (isTripleArray(posOrX)) return this.getBiomeAt(new Vector318(...posOrX));
18289
+ const pos = this.worldPosToChunkPos(posOrX);
18290
+ _Chunk.chunkPosToBlockIndex(pos);
18291
+ 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];
18292
+ }
18293
+ /** Copies 64 biome IDs in x + z * 4 + y * 16 order and refreshes placed blocks. Omit the array to clear biome data. */
18294
+ setBiomes(biomes) {
18295
+ const copy = biomes ? [...biomes] : void 0;
18296
+ if (copy && (copy.length !== 64 || !copy.every((id) => typeof id === "string" && id.length > 0))) {
18297
+ throw new RangeError("Section biomes must contain 64 nonempty biome IDs");
18298
+ }
18299
+ if (this.onBiomesChanged) return this.onBiomesChanged(copy);
18300
+ this.biomes = copy;
18301
+ return this.refreshBiomes();
18302
+ }
18303
+ /** Rebuilds biome-tinted blocks while preserving hidden blocks and unrelated render objects. */
18304
+ async refreshBiomes() {
18305
+ const affected = /* @__PURE__ */ new Map();
18306
+ const blocks = [];
18307
+ for (const index of this.renderedBlocks.keys()) {
18308
+ const block = this.data.get(index);
18309
+ const stateKey = ChunkData.paletteKey(block);
18310
+ let matches = affected.get(stateKey);
18311
+ if (!matches) {
18312
+ const key = AssetKey.parse("blockstates", block.type);
18313
+ matches = BlockStates.get(key).then(async (state) => {
18314
+ const origin = state?.key ?? key;
18315
+ const [source, fluid] = await Promise.all([BlockTints.biomeSource(origin), getBlockFluidState(origin, block.properties)]);
18316
+ return !!source || fluid?.kind === "water";
18317
+ });
18318
+ affected.set(stateKey, matches);
18319
+ }
18320
+ if (await matches) blocks.push({ index, block });
18321
+ }
18322
+ if (!blocks.length) return;
18323
+ const hidden = new Set(this.hiddenBlocks);
18324
+ try {
18325
+ await this.placeBlocks(blocks);
18326
+ } finally {
18327
+ for (const index of hidden) {
18328
+ this.hiddenBlocks.add(index);
18329
+ this.renderedBlocks.get(index)?.object?.setVisible(false);
18330
+ }
18331
+ const positions = blocks.map(({ index }) => new Vector318(...this.positionAt(index)));
18332
+ if (this.onBlocksChanged) await this.onBlocksChanged(positions);
18333
+ else await this.rebuildSectionMesh();
18334
+ }
18335
+ }
17702
18336
  async setBlockAt(posOrX, yOrBlock, z, block) {
17703
18337
  if (typeof posOrX == "number") {
17704
- return this.setBlockAt(new Vector317(posOrX, yOrBlock, z), block);
18338
+ return this.setBlockAt(new Vector318(posOrX, yOrBlock, z), block);
17705
18339
  }
17706
18340
  if (isTripleArray(posOrX)) {
17707
- return this.setBlockAt(new Vector317(posOrX[0], posOrX[1], posOrX[2]), yOrBlock);
18341
+ return this.setBlockAt(new Vector318(posOrX[0], posOrX[1], posOrX[2]), yOrBlock);
17708
18342
  }
17709
18343
  const worldPos = posOrX;
17710
18344
  const pos = this.worldPosToChunkPos(worldPos);
@@ -17737,14 +18371,15 @@ var Chunk = class _Chunk {
17737
18371
  nbt: block.nbt === void 0 ? void 0 : structuredClone(block.nbt)
17738
18372
  } : void 0
17739
18373
  }));
17740
- const states = new Map(placements.filter(({ key }) => key !== void 0).map(({ key, block }) => [key, block]));
17741
- const settled = await Promise.allSettled([...states.values()].map(async (stored) => {
17742
- const blockState3 = await BlockStates.get(AssetKey.parse("blockstates", stored.type));
17743
- const perBlock = !blockState3 || !this.sectionModels || !!(blockState3.key && BlockEntities.entry(await BlockEntities.getIndex(blockState3.key.root), blockState3.key.toNamespacedString()));
17744
- const fluid = await getBlockFluidState(blockState3?.key, stored.properties);
17745
- const prepared = perBlock || fluid?.renderModel === false ? void 0 : await this.sectionModels.prepareState(blockState3, stored.properties);
17746
- return { blockState: blockState3, perBlock, fluid, prepared, stored };
17747
- }));
18374
+ const states = /* @__PURE__ */ new Map();
18375
+ for (const { index, key, block } of placements) {
18376
+ if (key === void 0) continue;
18377
+ let state = states.get(key);
18378
+ if (!state) states.set(key, state = { stored: block, indices: [] });
18379
+ state.indices.push(index);
18380
+ }
18381
+ const biomes = /* @__PURE__ */ new Map();
18382
+ const settled = await Promise.allSettled([...states.values()].map(({ stored, indices }) => this.resolveState(stored, indices, biomes)));
17748
18383
  const resolutions = new Map([...states.keys()].map((key, i) => [key, settled[i]]));
17749
18384
  const positions = [];
17750
18385
  let failed = false, failure;
@@ -17757,7 +18392,7 @@ var Chunk = class _Chunk {
17757
18392
  if (this.fluidCells[index]) this.meshDirty = true;
17758
18393
  this.fluidCells[index] = 0;
17759
18394
  this.renderedBlocks.delete(index);
17760
- const worldPos = new Vector317(
18395
+ const worldPos = new Vector318(
17761
18396
  this.x * 16 + index % 16,
17762
18397
  this.y * 16 + Math.floor(index / 256),
17763
18398
  this.z * 16 + Math.floor(index / 16) % 16
@@ -17768,7 +18403,8 @@ var Chunk = class _Chunk {
17768
18403
  if (!readBlock) continue;
17769
18404
  const resolved = resolutions.get(key);
17770
18405
  if (resolved.status === "rejected") throw resolved.reason;
17771
- const { blockState: blockState3, perBlock, fluid, prepared, stored } = resolved.value;
18406
+ const { blockState: blockState3, perBlock, fluid, prepared, contexts, stored } = resolved.value;
18407
+ const context = contexts.get(index);
17772
18408
  if (!blockState3) {
17773
18409
  this.data.assign(index, 0);
17774
18410
  continue;
@@ -17776,9 +18412,10 @@ var Chunk = class _Chunk {
17776
18412
  const variantPosition = worldPos.toArray();
17777
18413
  if (fluid) {
17778
18414
  this.fluidCells[index] = (fluid.kind === "water" ? 16 : 32) | Math.min(fluid.level, 8);
18415
+ if (fluid.kind === "water") this.setWaterTint(index, context?.waterTint);
17779
18416
  this.meshDirty = true;
17780
18417
  }
17781
- const templates = perBlock ? void 0 : fluid?.renderModel === false ? [] : prepared?.pick(variantPosition);
18418
+ const templates = perBlock ? void 0 : fluid?.renderModel === false ? [] : (context?.prepared ?? prepared)?.pick(variantPosition);
17782
18419
  if (templates) {
17783
18420
  this.sectionBlocks.set(index, templates.map((template) => ({ index, template, cullMask: 0 })));
17784
18421
  this.meshDirty = true;
@@ -17788,7 +18425,9 @@ var Chunk = class _Chunk {
17788
18425
  instanceMeshes: true,
17789
18426
  maxInstanceCount: 2e3,
17790
18427
  initialState: stored.properties,
17791
- variantPosition
18428
+ variantPosition,
18429
+ biome: context?.biome,
18430
+ biomePosition: this.biomePosition(index, blockState3, stored)
17792
18431
  });
17793
18432
  object4.setPosition(MineRenderWorld.worldToScenePosition(worldPos));
17794
18433
  }
@@ -17811,24 +18450,21 @@ var Chunk = class _Chunk {
17811
18450
  for (const info of this.renderedBlocks.values()) info.object?.removeFromScene();
17812
18451
  this.sectionBlocks.clear();
17813
18452
  this.fluidCells.fill(0);
18453
+ this.waterTints = void 0;
17814
18454
  this.renderedBlocks.clear();
17815
18455
  this.hiddenBlocks.clear();
17816
18456
  this.meshDirty = true;
17817
18457
  this.data.copyFrom(data);
17818
- const ids = /* @__PURE__ */ new Set();
18458
+ const ids = /* @__PURE__ */ new Map();
17819
18459
  for (let index = 0; index < 4096; index++) {
17820
18460
  const id = this.data.idAt(index);
17821
- if (id) ids.add(id);
17822
- }
17823
- const palette = [...ids];
17824
- const settled = await Promise.allSettled(palette.map(async (id) => {
17825
- const state = this.data.stateAt(id);
17826
- const blockState3 = await BlockStates.get(AssetKey.parse("blockstates", state.type));
17827
- const perBlock = !blockState3 || !this.sectionModels || !!(blockState3.key && BlockEntities.entry(await BlockEntities.getIndex(blockState3.key.root), blockState3.key.toNamespacedString()));
17828
- const fluid = await getBlockFluidState(blockState3?.key, state.properties);
17829
- const prepared = perBlock || fluid?.renderModel === false ? void 0 : await this.sectionModels.prepareState(blockState3, state.properties);
17830
- return { blockState: blockState3, perBlock, fluid, prepared };
17831
- }));
18461
+ if (!id) continue;
18462
+ let indices = ids.get(id);
18463
+ if (!indices) ids.set(id, indices = []);
18464
+ indices.push(index);
18465
+ }
18466
+ const palette = [...ids.keys()], biomes = /* @__PURE__ */ new Map();
18467
+ const settled = await Promise.allSettled(palette.map((id) => this.resolveState(this.data.stateAt(id), ids.get(id), biomes)));
17832
18468
  const resolutions = new Map(palette.map((id, i) => [id, settled[i]]));
17833
18469
  const perBlocks = [];
17834
18470
  let failed = false, failure;
@@ -17838,24 +18474,28 @@ var Chunk = class _Chunk {
17838
18474
  try {
17839
18475
  const resolved = resolutions.get(id);
17840
18476
  if (resolved.status === "rejected") throw resolved.reason;
17841
- const { blockState: blockState3, perBlock, fluid, prepared } = resolved.value;
18477
+ const { blockState: blockState3, perBlock, fluid, prepared, contexts } = resolved.value;
18478
+ const context = contexts.get(index);
17842
18479
  if (!blockState3) {
17843
18480
  this.data.assign(index, 0);
17844
18481
  this.fluidCells[index] = 0;
17845
18482
  continue;
17846
18483
  }
17847
- if (fluid) this.fluidCells[index] = (fluid.kind === "water" ? 16 : 32) | Math.min(fluid.level, 8);
18484
+ if (fluid) {
18485
+ this.fluidCells[index] = (fluid.kind === "water" ? 16 : 32) | Math.min(fluid.level, 8);
18486
+ if (fluid.kind === "water") this.setWaterTint(index, context?.waterTint);
18487
+ }
17848
18488
  const variantPosition = [
17849
18489
  this.x * 16 + index % 16,
17850
18490
  this.y * 16 + Math.floor(index / 256),
17851
18491
  this.z * 16 + Math.floor(index / 16) % 16
17852
18492
  ];
17853
- const templates = perBlock ? void 0 : fluid?.renderModel === false ? [] : prepared?.pick(variantPosition);
18493
+ const templates = perBlock ? void 0 : fluid?.renderModel === false ? [] : (context?.prepared ?? prepared)?.pick(variantPosition);
17854
18494
  if (templates) {
17855
18495
  this.sectionBlocks.set(index, templates.map((template) => ({ index, template, cullMask: 0 })));
17856
18496
  this.meshDirty = true;
17857
18497
  } else {
17858
- perBlocks.push({ index, blockState: blockState3 });
18498
+ perBlocks.push({ index, blockState: blockState3, biome: context?.biome });
17859
18499
  }
17860
18500
  const readBlock = this.data.snapshot(index);
17861
18501
  this.renderedBlocks.set(index, { get block() {
@@ -17868,7 +18508,7 @@ var Chunk = class _Chunk {
17868
18508
  failed = true;
17869
18509
  }
17870
18510
  }
17871
- for (const { index, blockState: blockState3 } of perBlocks) {
18511
+ for (const { index, blockState: blockState3, biome } of perBlocks) {
17872
18512
  let object4;
17873
18513
  try {
17874
18514
  object4 = await this.scene.addBlock(blockState3, {
@@ -17876,13 +18516,11 @@ var Chunk = class _Chunk {
17876
18516
  instanceMeshes: true,
17877
18517
  maxInstanceCount: 2e3,
17878
18518
  initialState: this.data.stateAt(this.data.idAt(index)).properties,
17879
- variantPosition: [
17880
- this.x * 16 + index % 16,
17881
- this.y * 16 + Math.floor(index / 256),
17882
- this.z * 16 + Math.floor(index / 16) % 16
17883
- ]
18519
+ variantPosition: this.positionAt(index),
18520
+ biome,
18521
+ biomePosition: this.biomePosition(index, blockState3, this.data.stateAt(this.data.idAt(index)))
17884
18522
  });
17885
- object4.setPosition(new Vector317(
18523
+ object4.setPosition(new Vector318(
17886
18524
  this.x * 256 + index % 16 * 16,
17887
18525
  this.y * 256 + Math.floor(index / 256) * 16,
17888
18526
  this.z * 256 + Math.floor(index / 16) % 16 * 16
@@ -17899,6 +18537,68 @@ var Chunk = class _Chunk {
17899
18537
  }
17900
18538
  if (failed) throw failure;
17901
18539
  }
18540
+ positionAt(index) {
18541
+ return [this.x * 16 + index % 16, this.y * 16 + Math.floor(index / 256), this.z * 16 + Math.floor(index / 16) % 16];
18542
+ }
18543
+ biomePosition(index, blockState3, stored) {
18544
+ const position = this.positionAt(index), name = blockState3.key?.toNamespacedString();
18545
+ if (stored.properties?.half === "upper" && (name === "minecraft:tall_grass" || name === "minecraft:large_fern")) position[1]--;
18546
+ return position;
18547
+ }
18548
+ async resolveState(stored, indices, biomes) {
18549
+ const blockState3 = await BlockStates.get(AssetKey.parse("blockstates", stored.type));
18550
+ const perBlock = !blockState3 || !this.sectionModels || !!(blockState3.key && BlockEntities.entry(await BlockEntities.getIndex(blockState3.key.root), blockState3.key.toNamespacedString()));
18551
+ const fluid = await getBlockFluidState(blockState3?.key, stored.properties);
18552
+ 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;
18553
+ const source = hasBiomes && blockState3 ? await BlockTints.biomeSource(blockState3.key) : void 0;
18554
+ const prepare = (color2) => perBlock || fluid?.renderModel === false ? void 0 : this.sectionModels.prepareState(blockState3, stored.properties, color2);
18555
+ const contexts = /* @__PURE__ */ new Map();
18556
+ const groups = /* @__PURE__ */ new Map();
18557
+ if (blockState3 && (source || fluid?.kind === "water")) {
18558
+ for (const index of indices) {
18559
+ const position = this.biomePosition(index, blockState3, stored);
18560
+ const biome = this.biomeAt ? this.biomeAt(...position) : position[1] >= this.y * 16 ? this.getBiomeAt(position) : void 0;
18561
+ if (!biome) continue;
18562
+ const key = JSON.stringify(source === "grass" ? [biome, position[0], position[2]] : [biome]);
18563
+ let group = groups.get(key);
18564
+ if (!group) groups.set(key, group = { biome, position, indices: [] });
18565
+ group.indices.push(index);
18566
+ }
18567
+ }
18568
+ const groupedCount = [...groups.values()].reduce((sum, group) => sum + group.indices.length, 0);
18569
+ const prepared = groupedCount < indices.length ? await prepare() : void 0;
18570
+ await Promise.all([...groups.values()].map(async ({ biome, position, indices: indices2 }) => {
18571
+ const available = () => {
18572
+ const key = JSON.stringify([biome, blockState3?.key?.root]);
18573
+ let found = biomes.get(key);
18574
+ if (!found) {
18575
+ found = Biomes.get(biome, blockState3?.key).then((value) => value !== void 0);
18576
+ biomes.set(key, found);
18577
+ }
18578
+ return found;
18579
+ };
18580
+ const context = { biome };
18581
+ if (perBlock) {
18582
+ if (!await available()) context.biome = void 0;
18583
+ } else {
18584
+ context.prepared = await prepare(async () => await available() ? BlockTints.getBiomeColor(blockState3?.key, biome, position) : void 0);
18585
+ if (fluid?.kind === "water" && await available()) {
18586
+ const color2 = await Biomes.getColor(biome, "water", blockState3?.key);
18587
+ if (color2 !== void 0 && color2 !== 4159204) context.waterTint = new Color13(color2).toArray();
18588
+ }
18589
+ }
18590
+ for (const index of indices2) contexts.set(index, context);
18591
+ }));
18592
+ return { blockState: blockState3, perBlock, fluid, prepared, contexts, stored };
18593
+ }
18594
+ setWaterTint(index, tint) {
18595
+ if (!this.sectionModels || !this.waterTints && tint === void 0) return;
18596
+ if (!this.waterTints) {
18597
+ this.waterTints = new Float32Array(4096 * 3);
18598
+ for (let cell = 0; cell < 4096; cell++) this.waterTints.set(WATER_TINT2, cell * 3);
18599
+ }
18600
+ this.waterTints.set(tint ?? WATER_TINT2, index * 3);
18601
+ }
17902
18602
  /** Changes visibility at world block coordinates while preserving the block's data. */
17903
18603
  async setBlockVisibleAt(pos, visible) {
17904
18604
  const index = _Chunk.chunkPosToBlockIndex(this.worldPosToChunkPos(pos));
@@ -18002,7 +18702,7 @@ var Chunk = class _Chunk {
18002
18702
  if (block && !block.object) origins.set(kind, block.block.type);
18003
18703
  }
18004
18704
  if (origins.size) {
18005
- fluids = { cells: new Uint8Array(18 * 18 * 18) };
18705
+ fluids = { cells: new Uint8Array(18 * 18 * 18), waterTints: this.waterTints?.slice() };
18006
18706
  for (let y = -1; y <= 16; y++) {
18007
18707
  for (let z = -1; z <= 16; z++) {
18008
18708
  for (let x = -1; x <= 16; x++) {
@@ -18033,7 +18733,7 @@ var Chunk = class _Chunk {
18033
18733
  /** Removes stored blocks and render objects. Set `notify` to false to skip the owning world's update. */
18034
18734
  async clear(notify = true) {
18035
18735
  const positions = notify && this.onBlocksChanged ? [...this.renderedBlocks.keys()].map((index) => this.chunkPosToWorldPos(
18036
- new Vector317(index % 16, Math.floor(index / 256), Math.floor(index / 16) % 16)
18736
+ new Vector318(index % 16, Math.floor(index / 256), Math.floor(index / 16) % 16)
18037
18737
  )) : [];
18038
18738
  for (const info of this.renderedBlocks.values()) info.object?.removeFromScene();
18039
18739
  this.sectionBlocks.clear();
@@ -18044,8 +18744,13 @@ var Chunk = class _Chunk {
18044
18744
  this.renderedBlocks.clear();
18045
18745
  this.hiddenBlocks.clear();
18046
18746
  this.fluidCells.fill(0);
18747
+ this.waterTints = void 0;
18047
18748
  this.data.clear();
18048
- if (notify) await this.onBlocksChanged?.(positions);
18749
+ this.biomes = void 0;
18750
+ if (notify) {
18751
+ await this.onBiomesChanged?.(void 0);
18752
+ await this.onBlocksChanged?.(positions);
18753
+ }
18049
18754
  }
18050
18755
  async dispose() {
18051
18756
  await this.clear();
@@ -18061,14 +18766,14 @@ var Chunk = class _Chunk {
18061
18766
  return pos.y * 16 * 16 + pos.z * 16 + pos.x;
18062
18767
  }
18063
18768
  worldPosToChunkPos(pos) {
18064
- return new Vector317(
18769
+ return new Vector318(
18065
18770
  pos.x - this.x * 16,
18066
18771
  pos.y - this.y * 16,
18067
18772
  pos.z - this.z * 16
18068
18773
  );
18069
18774
  }
18070
18775
  chunkPosToWorldPos(pos) {
18071
- return new Vector317(
18776
+ return new Vector318(
18072
18777
  this.x * 16 + pos.x,
18073
18778
  this.y * 16 + pos.y,
18074
18779
  this.z * 16 + pos.z
@@ -18266,6 +18971,7 @@ export {
18266
18971
  BannerPatterns,
18267
18972
  BasicAssetKey,
18268
18973
  BatchedExecutor,
18974
+ Biomes,
18269
18975
  BlockEntities,
18270
18976
  BlockObject,
18271
18977
  BlockStateResolver,
@@ -18299,6 +19005,7 @@ export {
18299
19005
  EntityObject,
18300
19006
  Env,
18301
19007
  FluidModelObject,
19008
+ FlyControls,
18302
19009
  Fonts,
18303
19010
  GUI_CONTAINER_LAYOUTS,
18304
19011
  Geometries,
@@ -18401,6 +19108,7 @@ export {
18401
19108
  isDisposable,
18402
19109
  isDoubleArray,
18403
19110
  isEntityObject,
19111
+ isFlyControls,
18404
19112
  isGuiObject,
18405
19113
  isInstanceManager,
18406
19114
  isInstanceReference,