Add noise visualizations to biome map

This commit is contained in:
Misode
2021-10-29 02:28:55 +02:00
parent c75c5b7d65
commit 3e4d8d7df5
10 changed files with 216 additions and 86 deletions

View File

@@ -28,6 +28,7 @@ export const Octicon = {
plus: <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" width="16" height="16"><path fill-rule="evenodd" d="M8 2a.75.75 0 01.75.75v4.5h4.5a.75.75 0 010 1.5h-4.5v4.5a.75.75 0 01-1.5 0v-4.5h-4.5a.75.75 0 010-1.5h4.5v-4.5A.75.75 0 018 2z"></path></svg>,
plus_circle: <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" width="16" height="16"><path fill-rule="evenodd" d="M1.5 8a6.5 6.5 0 1113 0 6.5 6.5 0 01-13 0zM8 0a8 8 0 100 16A8 8 0 008 0zm.75 4.75a.75.75 0 00-1.5 0v2.5h-2.5a.75.75 0 000 1.5h2.5v2.5a.75.75 0 001.5 0v-2.5h2.5a.75.75 0 000-1.5h-2.5v-2.5z"></path></svg>,
search: <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" width="16" height="16"><path fill-rule="evenodd" d="M11.5 7a4.499 4.499 0 11-8.998 0A4.499 4.499 0 0111.5 7zm-.82 4.74a6 6 0 111.06-1.06l3.04 3.04a.75.75 0 11-1.06 1.06l-3.04-3.04z"></path></svg>,
stack: <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" width="16" height="16"><path fill-rule="evenodd" d="M7.122.392a1.75 1.75 0 011.756 0l5.003 2.902c.83.481.83 1.68 0 2.162L8.878 8.358a1.75 1.75 0 01-1.756 0L2.119 5.456a1.25 1.25 0 010-2.162L7.122.392zM8.125 1.69a.25.25 0 00-.25 0l-4.63 2.685 4.63 2.685a.25.25 0 00.25 0l4.63-2.685-4.63-2.685zM1.601 7.789a.75.75 0 011.025-.273l5.249 3.044a.25.25 0 00.25 0l5.249-3.044a.75.75 0 01.752 1.298l-5.248 3.044a1.75 1.75 0 01-1.756 0L1.874 8.814A.75.75 0 011.6 7.789zm0 3.5a.75.75 0 011.025-.273l5.249 3.044a.25.25 0 00.25 0l5.249-3.044a.75.75 0 01.752 1.298l-5.248 3.044a1.75 1.75 0 01-1.756 0l-5.248-3.044a.75.75 0 01-.273-1.025z"></path></svg>,
sun: <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" width="16" height="16"><path fill-rule="evenodd" d="M8 10.5a2.5 2.5 0 100-5 2.5 2.5 0 000 5zM8 12a4 4 0 100-8 4 4 0 000 8zM8 0a.75.75 0 01.75.75v1.5a.75.75 0 01-1.5 0V.75A.75.75 0 018 0zm0 13a.75.75 0 01.75.75v1.5a.75.75 0 01-1.5 0v-1.5A.75.75 0 018 13zM2.343 2.343a.75.75 0 011.061 0l1.06 1.061a.75.75 0 01-1.06 1.06l-1.06-1.06a.75.75 0 010-1.06zm9.193 9.193a.75.75 0 011.06 0l1.061 1.06a.75.75 0 01-1.06 1.061l-1.061-1.06a.75.75 0 010-1.061zM16 8a.75.75 0 01-.75.75h-1.5a.75.75 0 010-1.5h1.5A.75.75 0 0116 8zM3 8a.75.75 0 01-.75.75H.75a.75.75 0 010-1.5h1.5A.75.75 0 013 8zm10.657-5.657a.75.75 0 010 1.061l-1.061 1.06a.75.75 0 11-1.06-1.06l1.06-1.06a.75.75 0 011.06 0zm-9.193 9.193a.75.75 0 010 1.06l-1.06 1.061a.75.75 0 11-1.061-1.06l1.06-1.061a.75.75 0 011.061 0z"></path></svg>,
sync: <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" width="16" height="16"><path fill-rule="evenodd" d="M8 2.5a5.487 5.487 0 00-4.131 1.869l1.204 1.204A.25.25 0 014.896 6H1.25A.25.25 0 011 5.75V2.104a.25.25 0 01.427-.177l1.38 1.38A7.001 7.001 0 0114.95 7.16a.75.75 0 11-1.49.178A5.501 5.501 0 008 2.5zM1.705 8.005a.75.75 0 01.834.656 5.501 5.501 0 009.592 2.97l-1.204-1.204a.25.25 0 01.177-.427h3.646a.25.25 0 01.25.25v3.646a.25.25 0 01-.427.177l-1.38-1.38A7.001 7.001 0 011.05 8.84a.75.75 0 01.656-.834z"></path></svg>,
tag: <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" width="16" height="16"><path fill-rule="evenodd" d="M2.5 7.775V2.75a.25.25 0 01.25-.25h5.025a.25.25 0 01.177.073l6.25 6.25a.25.25 0 010 .354l-5.025 5.025a.25.25 0 01-.354 0l-6.25-6.25a.25.25 0 01-.073-.177zm-1.5 0V2.75C1 1.784 1.784 1 2.75 1h5.025c.464 0 .91.184 1.238.513l6.25 6.25a1.75 1.75 0 010 2.474l-5.026 5.026a1.75 1.75 0 01-2.474 0l-6.25-6.25A1.75 1.75 0 011 7.775zM6 5a1 1 0 100 2 1 1 0 000-2z"></path></svg>,

View File

@@ -3,15 +3,18 @@ import type { NoiseOctaves } from 'deepslate'
import { NoiseGeneratorSettings } from 'deepslate'
import { useEffect, useRef, useState } from 'preact/hooks'
import type { PreviewProps } from '.'
import { Btn } from '..'
import { Btn, BtnMenu } from '..'
import { useCanvas } from '../../hooks'
import { locale } from '../../Locales'
import { biomeMap, getBiome } from '../../previews'
import { newSeed } from '../../Utils'
const LAYERS = ['biomes', 'temperature', 'humidity', 'continentalness', 'erosion', 'weirdness', 'offset', 'factor', 'jaggedness'] as const
export const BiomeSourcePreview = ({ model, data, shown, lang, version }: PreviewProps) => {
const [scale, setScale] = useState(2)
const [focused, setFocused] = useState<string | undefined>(undefined)
const [layers, setLayers] = useState(new Set<typeof LAYERS[number]>(['biomes']))
const offset = useRef<[number, number]>([0, 0])
const res = useRef(1)
const refineTimeout = useRef<number>(undefined)
@@ -26,7 +29,7 @@ export const BiomeSourcePreview = ({ model, data, shown, lang, version }: Previe
return [200 / res.current, 200 / res.current]
},
async draw(img) {
const options = { octaves, biomeColors: {}, offset: offset.current, scale, seed, res: res.current, version }
const options = { octaves, biomeColors: {}, layers, offset: offset.current, scale, seed, res: res.current, version }
await biomeMap(data, img, options)
if (res.current === 4) {
clearTimeout(refineTimeout.current)
@@ -44,21 +47,21 @@ export const BiomeSourcePreview = ({ model, data, shown, lang, version }: Previe
redraw()
},
async onHover(x, y) {
const options = { octaves, biomeColors: {}, offset: offset.current, scale, seed, res: 1, version }
const options = { octaves, biomeColors: {}, layers, offset: offset.current, scale, seed, res: 1, version }
const biome = await getBiome(data, Math.floor(x * 200), Math.floor(y * 200), options)
setFocused(biome)
},
onLeave() {
setFocused(undefined)
},
}, [state, scale, seed])
}, [state, scale, seed, layers])
useEffect(() => {
if (shown) {
res.current = type === 'multi_noise' ? 4 : 1
redraw()
}
}, [state, scale, seed, shown])
}, [state, scale, seed, layers, shown])
const changeScale = (newScale: number) => {
offset.current[0] = offset.current[0] * scale / newScale
@@ -69,6 +72,19 @@ export const BiomeSourcePreview = ({ model, data, shown, lang, version }: Previe
return <>
<div class="controls">
{focused && <Btn label={focused} class="no-pointer" />}
{type === 'multi_noise' &&
<BtnMenu icon="stack" tooltip={locale(lang, 'configure_layers')}>
{LAYERS.map(name => {
const enabled = layers.has(name)
return <Btn label={locale(lang, `layer.${name}`)}
active={enabled}
tooltip={enabled ? locale(lang, 'enabled') : locale(lang, 'disabled')}
onClick={(e) => {
setLayers(new Set([name]))
e.stopPropagation()
}} />
})}
</BtnMenu>}
{(type === 'multi_noise' || type === 'checkerboard') && <>
<Btn icon="dash" tooltip={locale(lang, 'zoom_out')}
onClick={() => changeScale(scale * 1.5)} />
@@ -88,38 +104,40 @@ function calculateState(data: any, octaves: NoiseOctaves) {
}
function getOctaves(obj: any): NoiseOctaves {
if (typeof obj === 'string') {
switch (obj.replace(/^minecraft:/, '')) {
case 'overworld':
case 'amplified':
return {
temperature: { firstOctave: -9, amplitudes: [1.5, 0, 1, 0, 0, 0] },
humidity: { firstOctave: -7, amplitudes: [1, 1, 0, 0, 0, 0] },
continentalness: { firstOctave: -9, amplitudes: [1, 1, 2, 2, 2, 1, 1, 1, 1] },
erosion: { firstOctave: -9, amplitudes: [1, 1, 0, 1, 1] },
weirdness: { firstOctave: -7, amplitudes: [1, 2, 1, 0, 0, 0] },
shift: { firstOctave: -3, amplitudes: [1, 1, 1, 0] },
}
case 'end':
case 'floating_islands':
return {
temperature: { firstOctave: 0, amplitudes: [0] },
humidity: { firstOctave: 0, amplitudes: [0] },
continentalness: { firstOctave: 0, amplitudes: [0] },
erosion: { firstOctave: 0, amplitudes: [0] },
weirdness: { firstOctave: 0, amplitudes: [0] },
shift: { firstOctave: 0, amplitudes: [0] },
}
default:
return {
temperature: { firstOctave: -7, amplitudes: [1, 1] },
humidity: { firstOctave: -7, amplitudes: [1, 1] },
continentalness: { firstOctave: -7, amplitudes: [1, 1] },
erosion: { firstOctave: -7, amplitudes: [1, 1] },
weirdness: { firstOctave: -7, amplitudes: [1, 1] },
shift: { firstOctave: 0, amplitudes: [0] },
}
}
if (typeof obj !== 'string') {
const settings = NoiseGeneratorSettings.fromJson(obj)
obj = settings.noise.densityFactor === 0 && settings.noise.densityOffset === -0.030078125
? 'minecraft:nether' : 'minecraft:overworld'
}
switch (obj.replace(/^minecraft:/, '')) {
case 'overworld':
case 'amplified':
return {
temperature: { firstOctave: -9, amplitudes: [1.5, 0, 1, 0, 0, 0] },
humidity: { firstOctave: -7, amplitudes: [1, 1, 0, 0, 0, 0] },
continentalness: { firstOctave: -9, amplitudes: [1, 1, 2, 2, 2, 1, 1, 1, 1] },
erosion: { firstOctave: -9, amplitudes: [1, 1, 0, 1, 1] },
weirdness: { firstOctave: -7, amplitudes: [1, 2, 1, 0, 0, 0] },
shift: { firstOctave: -3, amplitudes: [1, 1, 1, 0] },
}
case 'end':
case 'floating_islands':
return {
temperature: { firstOctave: 0, amplitudes: [0] },
humidity: { firstOctave: 0, amplitudes: [0] },
continentalness: { firstOctave: 0, amplitudes: [0] },
erosion: { firstOctave: 0, amplitudes: [0] },
weirdness: { firstOctave: 0, amplitudes: [0] },
shift: { firstOctave: 0, amplitudes: [0] },
}
default:
return {
temperature: { firstOctave: -7, amplitudes: [1, 1] },
humidity: { firstOctave: -7, amplitudes: [1, 1] },
continentalness: { firstOctave: -7, amplitudes: [1, 1] },
erosion: { firstOctave: -7, amplitudes: [1, 1] },
weirdness: { firstOctave: -7, amplitudes: [1, 1] },
shift: { firstOctave: 0, amplitudes: [0] },
}
}
return NoiseGeneratorSettings.fromJson(obj).octaves
}

View File

@@ -1,7 +1,7 @@
import { DataModel } from '@mcschema/core'
import type { BiomeSource, Climate, NoiseOctaves } from 'deepslate'
import { FixedBiome, NoiseGeneratorSettings, NoiseSampler, NormalNoise, Random } from 'deepslate'
import init, { biome_parameters, climate_sampler, multi_noise } from 'deepslate-rs'
import type { NoiseOctaves } from 'deepslate'
import { FixedBiome, LegacyRandom, NormalNoise, TerrainShaper } from 'deepslate'
import init, { biome_parameters, climate_noise, climate_sampler, multi_noise } from 'deepslate-rs'
// @ts-expect-error
import wasm from 'deepslate-rs/deepslate_rs_bg.wasm?url'
import { fetchPreset } from '../DataFetcher'
@@ -16,7 +16,21 @@ async function loadWasm() {
console.debug(`Loaded deepslate-rs from "${wasm}"`)
}
type BiomeColors = Record<string, number[]>
const OverworldShaper = TerrainShaper.overworld()
const LAYERS = {
temperature: [-1, 1],
humidity: [-1, 1],
continentalness: [-1.1, 1],
erosion: [-1, 1],
weirdness: [-1, 1],
offset: [-1, 1],
factor: [0, 12],
jaggedness: [0, 1],
}
type Triple = [number, number, number]
type BiomeColors = Record<string, Triple>
type BiomeSourceOptions = {
octaves: NoiseOctaves,
biomeColors: BiomeColors,
@@ -25,18 +39,20 @@ type BiomeSourceOptions = {
res: number,
seed: bigint,
version: VersionId,
layers: Set<keyof typeof LAYERS | 'biomes'>,
}
interface CachedBiomeSource extends BiomeSource {
interface CachedBiomeSource {
getBiome(x: number, y: number, z: number): string
getBiomes?(xFrom: number, xTo: number, xStep: number, yFrom: number, yTo: number, yStep: number, zFrom: number, zTo: number, zStep: number): string[]
getClimate?(layers: Set<keyof typeof LAYERS>, xFrom: number, xTo: number, xStep: number, yFrom: number, yTo: number, yStep: number, zFrom: number, zTo: number, zStep: number): {[k: string]: number}[]
}
let cacheState: any
let biomeSourceCache: CachedBiomeSource
let climateSamplerCache: Climate.Sampler
export async function biomeMap(state: any, img: ImageData, options: BiomeSourceOptions) {
const { biomeSource, climateSampler } = await getCached(state, options)
const { biomeSource } = await getCached(state, options)
const data = img.data
const ox = -Math.round(options.offset[0]) - 100 + options.res / 2
@@ -44,19 +60,29 @@ export async function biomeMap(state: any, img: ImageData, options: BiomeSourceO
const row = img.width * 4 / options.res
const col = 4 / options.res
const biomes = biomeSource.getBiomes?.(
ox * options.scale, (200 + ox) * options.scale, options.res * options.scale,
64, 65, 1,
oz * options.scale, (200 + oz) * options.scale, options.res * options.scale,
)
const xRange: Triple = [ox * options.scale, (200 + ox) * options.scale, options.res * options.scale]
const zRange: Triple = [oz * options.scale, (200 + oz) * options.scale, options.res * options.scale]
const biomes = !options.layers.has('biomes') ? undefined : biomeSource.getBiomes?.(...xRange, 64, 65, 1, ...zRange)
const layers = [...options.layers].filter(l => l !== 'biomes') as (keyof typeof LAYERS)[]
const noise = layers.length === 0 ? undefined : biomeSource.getClimate?.(new Set(layers), ...xRange, 64, 65, 1, ...zRange)
for (let x = 0; x < 200; x += options.res) {
for (let z = 0; z < 200; z += options.res) {
const i = z * row + x * col
const j = (x / options.res) * 200 / options.res + z / options.res
const worldX = (x + ox) * options.scale
const worldZ = (z + oz) * options.scale
const b = biomes?.[(x / options.res) * 200 / options.res + z / options.res] ?? biomeSource.getBiome(worldX, 64, worldZ, climateSampler)
const color = getBiomeColor(b, options.biomeColors)
let color: Triple = [50, 50, 50]
if (options.layers.has('biomes')) {
const biome = biomes?.[j] ?? biomeSource.getBiome(worldX, 64, worldZ)
color = getBiomeColor(biome, options.biomeColors)
} else if (noise && layers[0]) {
const value = noise[j][layers[0]]
const [min, max] = LAYERS[layers[0]]
const brightness = (value - min) / (max - min) * 256
color = [brightness, brightness, brightness]
}
data[i] = color[0]
data[i + 1] = color[1]
data[i + 2] = color[2]
@@ -66,26 +92,21 @@ export async function biomeMap(state: any, img: ImageData, options: BiomeSourceO
}
export async function getBiome(state: any, x: number, z: number, options: BiomeSourceOptions): Promise<string | undefined> {
const { biomeSource, climateSampler } = await getCached(state, options)
const { biomeSource } = await getCached(state, options)
const [xx, zz] = toWorld([x, z], options)
return biomeSource.getBiome(xx, 64, zz, climateSampler)
return biomeSource.getBiome(xx, 64, zz)
}
async function getCached(state: any, options: BiomeSourceOptions): Promise<{ biomeSource: CachedBiomeSource, climateSampler: Climate.Sampler }> {
async function getCached(state: any, options: BiomeSourceOptions): Promise<{ biomeSource: CachedBiomeSource}> {
const newState = [state, options.octaves, `${options.seed}`, options.version]
if (!deepEqual(newState, cacheState)) {
cacheState = deepClone(newState)
biomeSourceCache = await getBiomeSource(state, options)
const settings = NoiseGeneratorSettings.fromJson({ octaves: options.octaves })
const noiseSampler = new NoiseSampler(4, 4, 32, biomeSourceCache, settings.noise, options.octaves, options.seed)
climateSamplerCache = noiseSampler.getClimate.bind(noiseSampler)
}
return {
biomeSource: biomeSourceCache,
climateSampler: climateSamplerCache,
}
}
@@ -149,13 +170,32 @@ async function getBiomeSource(state: any, options: BiomeSourceOptions): Promise<
const ids = multi_noise(parameters, sampler, xFrom, xTo, xStep, yFrom, yTo, yStep, zFrom, zTo, zStep)
return [...ids].map(id => BiomeIds.getA(id) ?? 'unknown')
},
getClimate(layers, xFrom, xTo, xStep, yFrom, yTo, yStep, zFrom, zTo, zStep) {
const climate = climate_noise(sampler, xFrom, xTo, xStep, yFrom, yTo, yStep, zFrom, zTo, zStep)
const result = []
for (let i = 0; i < climate.length; i += 7) {
const [t, h, c, e, w] = climate.slice(i, i + 5)
const point = TerrainShaper.point(c, e, w)
result.push({
temperature: t,
humidity: h,
continentalness: c,
erosion: e,
weirdness: w,
...layers.has('offset') && { offset: OverworldShaper.offset(point) },
...layers.has('factor') && { factor: OverworldShaper.factor(point) },
...layers.has('jaggedness') && { jaggedness: OverworldShaper.jaggedness(point) },
})
}
return result
},
}
} else {
const noise = ['altitude', 'temperature', 'humidity', 'weirdness']
.map((id, i) => {
const config = state[`${id}_noise`]
config.firstOctave = clamp(config.firstOctave ?? -7, -100, -1)
return new NormalNoise(new Random(options.seed + BigInt(i)), config)
return new NormalNoise(new LegacyRandom(options.seed + BigInt(i)), config)
})
if (!Array.isArray(state.biomes) || state.biomes.length === 0) {
return new FixedBiome('unknown')
@@ -180,7 +220,7 @@ async function getBiomeSource(state: any, options: BiomeSourceOptions): Promise<
throw new Error('Unknown biome source')
}
function getBiomeColor(biome: string, biomeColors: BiomeColors) {
function getBiomeColor(biome: string, biomeColors: BiomeColors): Triple {
if (!biome) {
return [128, 128, 128]
}
@@ -197,7 +237,7 @@ function toWorld([x, z]: [number, number], options: BiomeSourceOptions) {
return [xx, zz]
}
const VanillaColors: Record<string, [number, number, number]> = {
const VanillaColors: Record<string, Triple> = {
'minecraft:badlands': [217,69,21],
'minecraft:badlands_plateau': [202,140,101],
'minecraft:bamboo_jungle': [118,142,20],

View File

@@ -1,5 +1,6 @@
import { DataModel } from '@mcschema/core'
import { PerlinNoise, Random } from 'deepslate'
import type { Random } from 'deepslate'
import { LegacyRandom, PerlinNoise } from 'deepslate'
import type { VersionId } from '../Schemas'
import { clamp, stringToColor } from '../Utils'
@@ -35,7 +36,7 @@ export type DecoratorOptions = {
version: VersionId,
}
export function decorator(state: any, img: ImageData, options: DecoratorOptions) {
const random = new Random(options.seed)
const random = new LegacyRandom(options.seed)
const ctx: PlacementContext = {
placements: [],
features: [],

View File

@@ -82,7 +82,7 @@ function getCached(state: unknown, options: NoiseSettingsOptions) {
cacheState = deepClone(newState)
chunkCache = []
const biomeSource = new FixedBiome('unknown')
generatorCache = new NoiseChunkGenerator(options.seed, biomeSource, settings, shape)
generatorCache = new NoiseChunkGenerator(options.seed, biomeSource, settings)
}
return {
settings,

View File

@@ -1,5 +1,5 @@
import { DataModel } from '@mcschema/core'
import { NoiseParameters, NormalNoise, Random } from 'deepslate'
import { LegacyRandom, NoiseParameters, NormalNoise } from 'deepslate'
import type { VersionId } from '../Schemas'
export type NoiseOptions = {
@@ -10,7 +10,7 @@ export type NoiseOptions = {
}
export function normalNoise(state: any, img: ImageData, options: NoiseOptions) {
const random = new Random(options.seed)
const random = new LegacyRandom(options.seed)
const params = NoiseParameters.fromJson(DataModel.unwrapLists(state))
const noise = new NormalNoise(random, params)

View File

@@ -1,4 +1,4 @@
import { PerlinNoise, Random } from 'deepslate'
import { LegacyRandom, PerlinNoise } from 'deepslate'
import { clampedLerp, lerp2 } from '../../Utils'
export class NoiseChunkGenerator {
@@ -18,7 +18,7 @@ export class NoiseChunkGenerator {
private xOffset: number = 0
constructor(seed: bigint) {
const random = new Random(seed)
const random = new LegacyRandom(seed)
this.minLimitPerlinNoise = new PerlinNoise(random, -15, [1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1])
this.maxLimitPerlinNoise = new PerlinNoise(random, -15, [1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1])
this.mainPerlinNoise = new PerlinNoise(random, -7, [1, 1, 1, 1, 1, 1, 1, 1])

View File

@@ -5,13 +5,16 @@
"advancement": "Advancement",
"collapse": "Collapse",
"collapse_all": "Hold %0% to collapse all",
"configure_layers": "Configure layers",
"copy": "Copy",
"copied": "Copied!",
"copy_context": "Copy context",
"dimension_type": "Dimension Type",
"dimension": "Dimension",
"disabled": "Disabled",
"download": "Download",
"duplicate": "Duplicate",
"enabled": "Enabled",
"entries_hidden": "%0% entries hidden",
"entries_hidden.more": "Show %0% more",
"entries_hidden.all": "Show all",
@@ -31,6 +34,15 @@
"indentation.tabs": "Tabs",
"item_modifier": "Item Modifier",
"language": "Language",
"layer.biomes": "Biomes",
"layer.temperature": "Temperature",
"layer.humidity": "Humidity",
"layer.continentalness": "Continentalness",
"layer.erosion": "Erosion",
"layer.weirdness": "Weirdness",
"layer.offset": "Offset",
"layer.factor": "Factor",
"layer.jaggedness": "Jaggedness",
"loot_table": "Loot Table",
"more": "More",
"move_down": "Move down",