mirror of
https://github.com/misode/misode.github.io.git
synced 2026-04-24 07:37:10 +00:00
Support 1.18 (experimental) snapshots (#158)
* Half support 1.18-experimental-snapshot-1 * Fetch 1.18 presets and improve rendering of lists * Noise preview with deepslate * Biome preview with deepslate * Generalize canvas logic in one hook * Simplify useCanvas * Use mcschema for 1.18 * Improve noise settings preview controls * Fix build * Update deepslate and improve preview caching * Cleanup, remove old preview code * Couple seed between model and preview * Limit output to improve performance + copy feedback For the vanilla overworld dimension (200K lines), it took 2+ seconds to write the output to the textarea Now capped at 10K chars * Add surface_relative_threshold to decorator preview * Improve fixed list errors
This commit is contained in:
@@ -155,10 +155,11 @@ async function fetchDynamicRegistries(version: Version, target: CollectionRegist
|
||||
}
|
||||
|
||||
export async function fetchPreset(version: VersionId, registry: string, id: string) {
|
||||
console.debug(`[fetchPreset] ${id} ${registry} ${id}`)
|
||||
console.debug(`[fetchPreset] ${registry} ${id}`)
|
||||
const versionData = config.versions.find(v => v.id === version)!
|
||||
try {
|
||||
const res = await fetch(`${vanillaDatapackUrl}/${versionData.refs.vanilla_datapack_data}/data/minecraft/${registry}/${id}.json`)
|
||||
const url = `${vanillaDatapackUrl}/${versionData.refs.vanilla_datapack_data}/data/minecraft/${registry}/${id}.json`
|
||||
const res = await fetch(url)
|
||||
return await res.json()
|
||||
} catch (e) {
|
||||
console.warn(`Error occurred while fetching ${registry} preset ${id}:`, message(e))
|
||||
|
||||
@@ -2,6 +2,7 @@ import { render } from 'preact'
|
||||
import type { RouterOnChangeArgs } from 'preact-router'
|
||||
import { Router } from 'preact-router'
|
||||
import { useEffect, useState } from 'preact/hooks'
|
||||
import config from '../config.json'
|
||||
import '../styles/global.css'
|
||||
import '../styles/nodes.css'
|
||||
import { Analytics } from './Analytics'
|
||||
@@ -12,6 +13,8 @@ import type { VersionId } from './Schemas'
|
||||
import { Store } from './Store'
|
||||
import { cleanUrl } from './Utils'
|
||||
|
||||
const VERSIONS_IN_TITLE = 3
|
||||
|
||||
function Main() {
|
||||
const [lang, setLanguage] = useState<string>('en')
|
||||
const changeLanguage = async (language: string) => {
|
||||
@@ -51,7 +54,9 @@ function Main() {
|
||||
}
|
||||
|
||||
const [title, setTitle] = useState<string>(locale(lang, 'title.home'))
|
||||
const changeTitle = (title: string, versions = ['1.15', '1.16', '1.17']) => {
|
||||
const changeTitle = (title: string, versions?: VersionId[]) => {
|
||||
versions ??= config.versions.map(v => v.id as VersionId)
|
||||
versions.splice(0, versions.length - VERSIONS_IN_TITLE)
|
||||
document.title = `${title} Minecraft ${versions.join(', ')}`
|
||||
setTitle(title)
|
||||
}
|
||||
|
||||
@@ -3,11 +3,12 @@ import { DataModel } from '@mcschema/core'
|
||||
import * as java15 from '@mcschema/java-1.15'
|
||||
import * as java16 from '@mcschema/java-1.16'
|
||||
import * as java17 from '@mcschema/java-1.17'
|
||||
import * as java18 from '@mcschema/java-1.18'
|
||||
import config from '../config.json'
|
||||
import { fetchData } from './DataFetcher'
|
||||
import { message } from './Utils'
|
||||
|
||||
export const VersionIds = ['1.15', '1.16', '1.17'] as const
|
||||
export const VersionIds = ['1.15', '1.16', '1.17', '1.18'] as const
|
||||
export type VersionId = typeof VersionIds[number]
|
||||
|
||||
export type BlockStateRegistry = {
|
||||
@@ -43,6 +44,7 @@ const versionGetter: {
|
||||
1.15: java15,
|
||||
1.16: java16,
|
||||
1.17: java17,
|
||||
1.18: java18,
|
||||
}
|
||||
|
||||
async function getVersion(id: VersionId): Promise<VersionData> {
|
||||
|
||||
@@ -39,7 +39,7 @@ export namespace Store {
|
||||
if (version) localStorage.setItem(ID_VERSION, version)
|
||||
}
|
||||
|
||||
export function setIndent(indent: string) {
|
||||
export function setIndent(indent: string | undefined) {
|
||||
if (indent) localStorage.setItem(ID_INDENT, indent)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,3 +1,6 @@
|
||||
import type { DataModel } from '@mcschema/core'
|
||||
import { Path } from '@mcschema/core'
|
||||
import rfdc from 'rfdc'
|
||||
import config from '../config.json'
|
||||
|
||||
export function isPromise(obj: any): obj is Promise<any> {
|
||||
@@ -12,6 +15,16 @@ export function hexId(length = 12) {
|
||||
return Array.from(arr, dec2hex).join('')
|
||||
}
|
||||
|
||||
export function randomSeed() {
|
||||
return BigInt(Math.floor(Math.random() * Number.MAX_SAFE_INTEGER))
|
||||
}
|
||||
|
||||
export function newSeed(model: DataModel) {
|
||||
const seed = Math.floor(Math.random() * (4294967296)) - 2147483648
|
||||
model.set(new Path(['generator', 'seed']), seed, true)
|
||||
model.set(new Path(['generator', 'biome_source', 'seed']), seed)
|
||||
}
|
||||
|
||||
export function htmlEncode(str: string) {
|
||||
return str.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>')
|
||||
.replace(/"/g, '"').replace(/'/g, ''').replace(/\//g, '/')
|
||||
@@ -38,6 +51,10 @@ export function stringToColor(str: string): [number, number, number] {
|
||||
return [h % 256, (h >> 8) % 256, (h >> 16) % 256]
|
||||
}
|
||||
|
||||
export function square(a: number) {
|
||||
return a * a
|
||||
}
|
||||
|
||||
export function clamp(a: number, b: number, c: number) {
|
||||
return Math.max(a, Math.min(b, c))
|
||||
}
|
||||
@@ -72,3 +89,42 @@ export function message(e: unknown): string {
|
||||
if (e instanceof Error) return e.message
|
||||
return `${e}`
|
||||
}
|
||||
|
||||
export const deepClone = rfdc()
|
||||
|
||||
/**
|
||||
* MIT License
|
||||
*
|
||||
* Copyright (c) 2017 Evgeny Poberezkin
|
||||
*
|
||||
* https://github.com/epoberezkin/fast-deep-equal/blob/master/LICENSE
|
||||
*/
|
||||
export function deepEqual(a: any, b: any) {
|
||||
if (a === b) return true
|
||||
|
||||
if (a && b && typeof a == 'object' && typeof b == 'object') {
|
||||
if (a.constructor !== b.constructor) return false
|
||||
let length, i
|
||||
if (Array.isArray(a)) {
|
||||
length = a.length
|
||||
if (length != b.length) return false
|
||||
for (i = length; i-- !== 0;) {
|
||||
if (!deepEqual(a[i], b[i])) return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
if (a.valueOf !== Object.prototype.valueOf) return a.valueOf() === b.valueOf()
|
||||
if (a.toString !== Object.prototype.toString) return a.toString() === b.toString()
|
||||
const keys = Object.keys(a)
|
||||
length = keys.length
|
||||
if (length !== Object.keys(b).length) return false
|
||||
for (i = length; i-- !== 0;)
|
||||
if (!Object.prototype.hasOwnProperty.call(b, keys[i])) return false
|
||||
for (i = length; i-- !== 0;) {
|
||||
const key = keys[i]
|
||||
if (!deepEqual(a[key], b[key])) return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
return a !== a && b !== b
|
||||
}
|
||||
|
||||
@@ -5,17 +5,14 @@ type BtnInputProps = {
|
||||
icon?: keyof typeof Octicon,
|
||||
label?: string,
|
||||
large?: boolean,
|
||||
type?: 'number' | 'text',
|
||||
doSelect?: number,
|
||||
value?: string,
|
||||
onChange?: (value: string) => unknown,
|
||||
}
|
||||
export function BtnInput({ icon, label, large, type, doSelect, value, onChange }: BtnInputProps) {
|
||||
export function BtnInput({ icon, label, large, doSelect, value, onChange }: BtnInputProps) {
|
||||
const onInput = onChange === undefined ? () => {} : (e: any) => {
|
||||
const value = (e.target as HTMLInputElement).value
|
||||
if (type !== 'number' || (!value.endsWith('.') && !isNaN(Number(value)))) {
|
||||
onChange?.(value)
|
||||
}
|
||||
onChange?.(value)
|
||||
}
|
||||
|
||||
const ref = useRef<HTMLInputElement>(null)
|
||||
@@ -28,6 +25,6 @@ export function BtnInput({ icon, label, large, type, doSelect, value, onChange }
|
||||
return <div class={`btn btn-input ${large ? 'large-input' : ''}`} onClick={e => e.stopPropagation()}>
|
||||
{icon && Octicon[icon]}
|
||||
{label && <span>{label}</span>}
|
||||
<input ref={ref} type="text" value={value} onInput={onInput} />
|
||||
<input ref={ref} type="text" value={value} onChange={onInput} />
|
||||
</div>
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@ export const Octicon = {
|
||||
archive: <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" width="16" height="16"><path fill-rule="evenodd" d="M1.75 2.5a.25.25 0 00-.25.25v1.5c0 .138.112.25.25.25h12.5a.25.25 0 00.25-.25v-1.5a.25.25 0 00-.25-.25H1.75zM0 2.75C0 1.784.784 1 1.75 1h12.5c.966 0 1.75.784 1.75 1.75v1.5A1.75 1.75 0 0114.25 6H1.75A1.75 1.75 0 010 4.25v-1.5zM1.75 7a.75.75 0 01.75.75v5.5c0 .138.112.25.25.25h10.5a.25.25 0 00.25-.25v-5.5a.75.75 0 111.5 0v5.5A1.75 1.75 0 0113.25 15H2.75A1.75 1.75 0 011 13.25v-5.5A.75.75 0 011.75 7zm4.5 1a.75.75 0 000 1.5h3.5a.75.75 0 100-1.5h-3.5z"></path></svg>,
|
||||
arrow_left: <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" width="16" height="16"><path fill-rule="evenodd" d="M7.78 12.53a.75.75 0 01-1.06 0L2.47 8.28a.75.75 0 010-1.06l4.25-4.25a.75.75 0 011.06 1.06L4.81 7h7.44a.75.75 0 010 1.5H4.81l2.97 2.97a.75.75 0 010 1.06z"></path></svg>,
|
||||
arrow_right: <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" width="16" height="16"><path fill-rule="evenodd" d="M8.22 2.97a.75.75 0 011.06 0l4.25 4.25a.75.75 0 010 1.06l-4.25 4.25a.75.75 0 01-1.06-1.06l2.97-2.97H3.75a.75.75 0 010-1.5h7.44L8.22 4.03a.75.75 0 010-1.06z"></path></svg>,
|
||||
check: <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" width="16" height="16"><path fill-rule="evenodd" d="M13.78 4.22a.75.75 0 010 1.06l-7.25 7.25a.75.75 0 01-1.06 0L2.22 9.28a.75.75 0 011.06-1.06L6 10.94l6.72-6.72a.75.75 0 011.06 0z"></path></svg>,
|
||||
chevron_down: <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" width="16" height="16"><path fill-rule="evenodd" d="M12.78 6.22a.75.75 0 010 1.06l-4.25 4.25a.75.75 0 01-1.06 0L3.22 7.28a.75.75 0 011.06-1.06L8 9.94l3.72-3.72a.75.75 0 011.06 0z"></path></svg>,
|
||||
chevron_right: <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" width="16" height="16"><path fill-rule="evenodd" d="M6.22 3.22a.75.75 0 011.06 0l4.25 4.25a.75.75 0 010 1.06l-4.25 4.25a.75.75 0 01-1.06-1.06L9.94 8 6.22 4.28a.75.75 0 010-1.06z"></path></svg>,
|
||||
chevron_up: <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" width="16" height="16"><path fill-rule="evenodd" d="M3.22 9.78a.75.75 0 010-1.06l4.25-4.25a.75.75 0 011.06 0l4.25 4.25a.75.75 0 01-1.06 1.06L8 6.06 4.28 9.78a.75.75 0 01-1.06 0z"></path></svg>,
|
||||
|
||||
@@ -28,7 +28,7 @@ export function PreviewPanel({ lang, model, version, id, shown }: PreviewPanelPr
|
||||
}
|
||||
|
||||
if (id === 'worldgen/noise_settings' && model) {
|
||||
const data = model.get(new Path(['noise']))
|
||||
const data = model.get(new Path([]))
|
||||
if (data) return <NoiseSettingsPreview {...{ lang, model, version, shown, data }} />
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import type { DataModel } from '@mcschema/core'
|
||||
import { ModelPath } from '@mcschema/core'
|
||||
import { useEffect, useRef, useState } from 'preact/hooks'
|
||||
import { useCallback, useEffect, useRef, useState } from 'preact/hooks'
|
||||
import { Btn, BtnMenu } from '.'
|
||||
import { useModel } from '../hooks'
|
||||
import { locale } from '../Locales'
|
||||
@@ -9,6 +9,8 @@ import type { BlockStateRegistry } from '../Schemas'
|
||||
import { Store } from '../Store'
|
||||
import { message } from '../Utils'
|
||||
|
||||
const OUTPUT_CHARS_LIMIT = 10000
|
||||
|
||||
const INDENT: Record<string, number | string> = {
|
||||
'2_spaces': 2,
|
||||
'4_spaces': 4,
|
||||
@@ -23,22 +25,31 @@ type SourcePanelProps = {
|
||||
doCopy?: number,
|
||||
doDownload?: number,
|
||||
doImport?: number,
|
||||
copySuccess: () => unknown,
|
||||
onError: (message: string) => unknown,
|
||||
}
|
||||
export function SourcePanel({ lang, name, model, blockStates, doCopy, doDownload, doImport, onError }: SourcePanelProps) {
|
||||
export function SourcePanel({ lang, name, model, blockStates, doCopy, doDownload, doImport, copySuccess, onError }: SourcePanelProps) {
|
||||
const loc = locale.bind(null, lang)
|
||||
const [indent, setIndent] = useState(Store.getIndent())
|
||||
const source = useRef<HTMLTextAreaElement>(null)
|
||||
const download = useRef<HTMLAnchorElement>(null)
|
||||
const retransform = useRef<Function>()
|
||||
|
||||
const getOutput = useCallback((model: DataModel, blockStates: BlockStateRegistry) => {
|
||||
const data = model.schema.hook(transformOutput, new ModelPath(model), model.data, { blockStates })
|
||||
return JSON.stringify(data, null, INDENT[indent]) + '\n'
|
||||
}, [])
|
||||
|
||||
useEffect(() => {
|
||||
retransform.current = () => {
|
||||
if (!model || !blockStates) return
|
||||
try {
|
||||
const props = { blockStates: blockStates ?? {} }
|
||||
const data = model.schema.hook(transformOutput, new ModelPath(model), model.data, props)
|
||||
source.current.value = JSON.stringify(data, null, INDENT[indent]) + '\n'
|
||||
const output = getOutput(model, blockStates)
|
||||
if (output.length >= OUTPUT_CHARS_LIMIT) {
|
||||
source.current.value = output.slice(0, OUTPUT_CHARS_LIMIT) + `\n\nOutput is too large to display (+${OUTPUT_CHARS_LIMIT} chars)\nExport to view complete output\n\n`
|
||||
} else {
|
||||
source.current.value = output
|
||||
}
|
||||
} catch (e) {
|
||||
onError(`Error getting JSON output: ${message(e)}`)
|
||||
console.error(e)
|
||||
@@ -68,9 +79,10 @@ export function SourcePanel({ lang, name, model, blockStates, doCopy, doDownload
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
if (doCopy && source.current) {
|
||||
source.current.select()
|
||||
document.execCommand('copy')
|
||||
if (doCopy && model && blockStates) {
|
||||
navigator.clipboard.writeText(getOutput(model, blockStates)).then(() => {
|
||||
copySuccess()
|
||||
})
|
||||
}
|
||||
}, [doCopy])
|
||||
|
||||
|
||||
@@ -1,75 +1,67 @@
|
||||
import type { DataModel } from '@mcschema/core'
|
||||
import { Path } from '@mcschema/core'
|
||||
import type { NoiseOctaves } from 'deepslate'
|
||||
import { NoiseGeneratorSettings } from 'deepslate'
|
||||
import { useEffect, useRef, useState } from 'preact/hooks'
|
||||
import type { PreviewProps } from '.'
|
||||
import { Btn } from '..'
|
||||
import { useOnDrag, useOnHover } from '../../hooks'
|
||||
import { biomeSource, getBiome } from '../../previews'
|
||||
import { hexId } from '../../Utils'
|
||||
import { useCanvas } from '../../hooks'
|
||||
import { biomeMap, getBiome } from '../../previews'
|
||||
import { newSeed } from '../../Utils'
|
||||
|
||||
type BiomeSourceProps = {
|
||||
lang: string,
|
||||
model: DataModel,
|
||||
data: any,
|
||||
shown: boolean,
|
||||
}
|
||||
export const BiomeSourcePreview = ({ data, shown }: BiomeSourceProps) => {
|
||||
export const BiomeSourcePreview = ({ model, data, shown, version }: PreviewProps) => {
|
||||
const [scale, setScale] = useState(2)
|
||||
const [seed, setSeed] = useState(hexId())
|
||||
const [focused, setFocused] = useState<string | undefined>(undefined)
|
||||
const offset = useRef<[number, number]>([0, 0])
|
||||
const res = useRef(1)
|
||||
const refineTimeout = useRef<number>(undefined)
|
||||
|
||||
const seed = BigInt(model.get(new Path(['generator', 'seed'])))
|
||||
const octaves = getOctaves(model.get(new Path(['generator', 'settings'])))
|
||||
const state = calculateState(data, octaves)
|
||||
const type: string = data.type?.replace(/^minecraft:/, '')
|
||||
|
||||
const canvas = useRef<HTMLCanvasElement>(null)
|
||||
const offset = useRef<[number, number]>([0, 0])
|
||||
const redrawTimeout = useRef(undefined)
|
||||
const redraw = useRef<Function>()
|
||||
const refocus = useRef<Function>()
|
||||
|
||||
useEffect(() => {
|
||||
redraw.current = (res = 4) => {
|
||||
if (type !== 'multi_noise') res = 1
|
||||
const ctx = canvas.current.getContext('2d')!
|
||||
canvas.current.width = 200 / res
|
||||
canvas.current.height = 200 / res
|
||||
const img = ctx.createImageData(canvas.current.width, canvas.current.height)
|
||||
biomeSource(data, img, { biomeColors: {}, offset: offset.current, scale, seed, res })
|
||||
ctx.putImageData(img, 0, 0)
|
||||
if (res !== 1) {
|
||||
clearTimeout(redrawTimeout.current)
|
||||
redrawTimeout.current = setTimeout(() => redraw.current(1), 150) as any
|
||||
const { canvas, redraw } = useCanvas({
|
||||
size() {
|
||||
return [200 / res.current, 200 / res.current]
|
||||
},
|
||||
async draw(img) {
|
||||
const options = { octaves, biomeColors: {}, offset: offset.current, scale, seed, res: res.current, version }
|
||||
await biomeMap(data, img, options)
|
||||
if (res.current === 4) {
|
||||
clearTimeout(refineTimeout.current)
|
||||
refineTimeout.current = setTimeout(() => {
|
||||
res.current = 1
|
||||
redraw()
|
||||
}, 150)
|
||||
}
|
||||
}
|
||||
refocus.current = (x: number, y: number) => {
|
||||
const x2 = x * 200 / canvas.current.clientWidth
|
||||
const y2 = y * 200 / canvas.current.clientHeight
|
||||
const biome = getBiome(data, x2, y2, { biomeColors: {}, offset: offset.current, scale, seed, res: 1 })
|
||||
},
|
||||
async onDrag(dx, dy) {
|
||||
offset.current[0] = offset.current[0] + dx * 200
|
||||
offset.current[1] = offset.current[1] + dy * 200
|
||||
clearTimeout(refineTimeout.current)
|
||||
res.current = type === 'multi_noise' ? 4 : 1
|
||||
redraw()
|
||||
},
|
||||
async onHover(x, y) {
|
||||
const options = { octaves, biomeColors: {}, offset: offset.current, scale, seed, res: 1, version }
|
||||
const biome = await getBiome(data, Math.floor(x * 200), Math.floor(y * 200), options)
|
||||
setFocused(biome)
|
||||
}
|
||||
})
|
||||
|
||||
useOnDrag(canvas.current, (dx, dy) => {
|
||||
const x = dx * 200 / canvas.current.clientWidth
|
||||
const y = dy * 200 / canvas.current.clientHeight
|
||||
offset.current = [offset.current[0] + x, offset.current[1] + y]
|
||||
redraw.current()
|
||||
})
|
||||
|
||||
useOnHover(canvas.current, (x, y) => {
|
||||
if (x === undefined || y === undefined) {
|
||||
},
|
||||
onLeave() {
|
||||
setFocused(undefined)
|
||||
} else {
|
||||
refocus.current(x, y)
|
||||
}
|
||||
})
|
||||
},
|
||||
}, [state, scale, seed])
|
||||
|
||||
const state = JSON.stringify(data)
|
||||
useEffect(() => {
|
||||
if (shown) {
|
||||
redraw.current()
|
||||
res.current = type === 'multi_noise' ? 4 : 1
|
||||
redraw()
|
||||
}
|
||||
}, [state, scale, seed, shown])
|
||||
|
||||
const changeScale = (newScale: number) => {
|
||||
offset.current[0] *= scale / newScale
|
||||
offset.current[1] *= scale / newScale
|
||||
offset.current[0] = offset.current[0] * scale / newScale
|
||||
offset.current[1] = offset.current[1] * scale / newScale
|
||||
setScale(newScale)
|
||||
}
|
||||
|
||||
@@ -81,8 +73,49 @@ export const BiomeSourcePreview = ({ data, shown }: BiomeSourceProps) => {
|
||||
<Btn icon="plus" onClick={() => changeScale(scale / 1.5)} />
|
||||
</>}
|
||||
{type === 'multi_noise' &&
|
||||
<Btn icon="sync" onClick={() => setSeed(hexId())} />}
|
||||
<Btn icon="sync" onClick={() => newSeed(model)} />}
|
||||
</div>
|
||||
<canvas ref={canvas} width="200" height="200"></canvas>
|
||||
</>
|
||||
}
|
||||
|
||||
function calculateState(data: any, octaves: NoiseOctaves) {
|
||||
return JSON.stringify([data, octaves])
|
||||
}
|
||||
|
||||
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] },
|
||||
}
|
||||
}
|
||||
}
|
||||
return NoiseGeneratorSettings.fromJson(obj).octaves
|
||||
}
|
||||
|
||||
@@ -1,39 +1,27 @@
|
||||
import type { DataModel } from '@mcschema/core'
|
||||
import { useEffect, useRef, useState } from 'preact/hooks'
|
||||
import { useEffect, useState } from 'preact/hooks'
|
||||
import type { PreviewProps } from '.'
|
||||
import { Btn } from '..'
|
||||
import { useCanvas } from '../../hooks'
|
||||
import { decorator } from '../../previews'
|
||||
import type { VersionId } from '../../Schemas'
|
||||
import { hexId } from '../../Utils'
|
||||
import { randomSeed } from '../../Utils'
|
||||
|
||||
type DecoratorProps = {
|
||||
lang: string,
|
||||
model: DataModel,
|
||||
data: any,
|
||||
version: VersionId,
|
||||
shown: boolean,
|
||||
}
|
||||
export const DecoratorPreview = ({ data, version, shown }: DecoratorProps) => {
|
||||
export const DecoratorPreview = ({ data, version, shown }: PreviewProps) => {
|
||||
const [scale, setScale] = useState(4)
|
||||
const [seed, setSeed] = useState(hexId())
|
||||
const [seed, setSeed] = useState(randomSeed())
|
||||
|
||||
const canvas = useRef<HTMLCanvasElement>(null)
|
||||
const redraw = useRef<Function>()
|
||||
|
||||
useEffect(() => {
|
||||
redraw.current = () => {
|
||||
const ctx = canvas.current.getContext('2d')!
|
||||
canvas.current.width = scale * 16
|
||||
canvas.current.height = scale * 16
|
||||
const img = ctx.createImageData(canvas.current.width, canvas.current.height)
|
||||
const { canvas, redraw } = useCanvas({
|
||||
size() {
|
||||
return [scale * 16, scale * 16]
|
||||
},
|
||||
async draw(img) {
|
||||
decorator(data, img, { seed, version, size: [scale * 16, 128, scale * 16] })
|
||||
ctx.putImageData(img, 0, 0)
|
||||
}
|
||||
},
|
||||
})
|
||||
|
||||
const state = JSON.stringify(data)
|
||||
useEffect(() => {
|
||||
if (shown) {
|
||||
setTimeout(() => redraw.current())
|
||||
redraw()
|
||||
}
|
||||
}, [state, scale, seed, shown])
|
||||
|
||||
@@ -41,7 +29,7 @@ export const DecoratorPreview = ({ data, version, shown }: DecoratorProps) => {
|
||||
<div class="controls">
|
||||
<Btn icon="dash" onClick={() => setScale(Math.min(16, scale + 1))} />
|
||||
<Btn icon="plus" onClick={() => setScale(Math.max(1, scale - 1))} />
|
||||
<Btn icon="sync" onClick={() => setSeed(hexId())} />
|
||||
<Btn icon="sync" onClick={() => setSeed(randomSeed())} />
|
||||
</div>
|
||||
<canvas ref={canvas} width="64" height="64"></canvas>
|
||||
</>
|
||||
|
||||
@@ -1,60 +1,71 @@
|
||||
import type { DataModel } from '@mcschema/core'
|
||||
import { useEffect, useRef, useState } from 'preact/hooks'
|
||||
import type { PreviewProps } from '.'
|
||||
import { Btn, BtnInput, BtnMenu } from '..'
|
||||
import { useOnDrag } from '../../hooks'
|
||||
import { useCanvas } from '../../hooks'
|
||||
import { locale } from '../../Locales'
|
||||
import { noiseSettings } from '../../previews'
|
||||
import { hexId } from '../../Utils'
|
||||
import { checkVersion } from '../../Schemas'
|
||||
import { randomSeed } from '../../Utils'
|
||||
|
||||
type NoiseSettingsProps = {
|
||||
lang: string,
|
||||
model: DataModel,
|
||||
data: any,
|
||||
shown: boolean,
|
||||
}
|
||||
export const NoiseSettingsPreview = ({ lang, data, shown }: NoiseSettingsProps) => {
|
||||
export const NoiseSettingsPreview = ({ lang, data, shown, version }: PreviewProps) => {
|
||||
const loc = locale.bind(null, lang)
|
||||
const [seed, setSeed] = useState(hexId())
|
||||
const [biomeDepth, setBiomeDepth] = useState(0.1)
|
||||
const [biomeScale, setBiomeScale] = useState(0.2)
|
||||
|
||||
const canvas = useRef<HTMLCanvasElement>(null)
|
||||
const offset = useRef<number>(0)
|
||||
const redraw = useRef<Function>()
|
||||
const [seed, setSeed] = useState(randomSeed())
|
||||
const [biomeFactor, setBiomeFactor] = useState(0.2)
|
||||
const [biomeOffset, setBiomeOffset] = useState(0.1)
|
||||
const [biomePeaks, setBiomePeaks] = useState(0)
|
||||
const [focused, setFocused] = useState<string | undefined>(undefined)
|
||||
const offset = useRef(0)
|
||||
const state = JSON.stringify([data, biomeFactor, biomeOffset, biomePeaks])
|
||||
|
||||
const hasPeaks = checkVersion(version, '1.18')
|
||||
useEffect(() => {
|
||||
redraw.current = () => {
|
||||
const ctx = canvas.current.getContext('2d')!
|
||||
const size = data.height
|
||||
canvas.current.width = size
|
||||
canvas.current.height = size
|
||||
const img = ctx.createImageData(canvas.current.width, canvas.current.height)
|
||||
noiseSettings(data, img, { biomeDepth, biomeScale, offset: offset.current, width: size, seed })
|
||||
ctx.putImageData(img, 0, 0)
|
||||
}
|
||||
})
|
||||
setBiomeFactor(hasPeaks ? 600 : 0.2)
|
||||
setBiomeOffset(hasPeaks ? 0.05 : 0.1)
|
||||
}, [hasPeaks])
|
||||
|
||||
useOnDrag(canvas.current, (dx) => {
|
||||
const x = dx * canvas.current.width / canvas.current.clientWidth
|
||||
offset.current = offset.current + x
|
||||
redraw.current()
|
||||
})
|
||||
const size = data?.noise?.height ?? 256
|
||||
const { canvas, redraw } = useCanvas({
|
||||
size() {
|
||||
return [size, size]
|
||||
},
|
||||
async draw(img) {
|
||||
const options = { biomeOffset, biomeFactor, biomePeaks, offset: offset.current, width: img.width, seed, version }
|
||||
noiseSettings(data, img, options)
|
||||
},
|
||||
async onDrag(dx) {
|
||||
offset.current += dx * size
|
||||
redraw()
|
||||
},
|
||||
async onHover(_, y) {
|
||||
const worldY = size - Math.max(1, Math.ceil(y * size)) + (data?.noise?.min_y ?? 0)
|
||||
setFocused(`${worldY}`)
|
||||
},
|
||||
onLeave() {
|
||||
setFocused(undefined)
|
||||
},
|
||||
}, [state, seed])
|
||||
|
||||
const state = JSON.stringify(data)
|
||||
useEffect(() => {
|
||||
if (shown) {
|
||||
redraw.current()
|
||||
redraw()
|
||||
}
|
||||
}, [state, biomeDepth, biomeScale, seed, shown])
|
||||
}, [state, seed, shown])
|
||||
|
||||
return <>
|
||||
<div class="controls">
|
||||
{focused && <Btn label={`Y = ${focused}`} class="no-pointer" />}
|
||||
<BtnMenu icon="gear">
|
||||
<BtnInput type="number" label={loc('preview.depth')} value={`${biomeDepth}`} onChange={v => setBiomeDepth(Number(v))} />
|
||||
<BtnInput type="number" label={loc('preview.scale')} value={`${biomeScale}`} onChange={v => setBiomeScale(Number(v))} />
|
||||
{hasPeaks ? <>
|
||||
<BtnInput label={loc('preview.factor')} value={`${biomeFactor}`} onChange={v => setBiomeFactor(Number(v))} />
|
||||
<BtnInput label={loc('preview.offset')} value={`${biomeOffset}`} onChange={v => setBiomeOffset(Number(v))} />
|
||||
<BtnInput label={loc('preview.peaks')} value={`${biomePeaks}`} onChange={v => setBiomePeaks(Number(v))} />
|
||||
</> : <>
|
||||
<BtnInput label={loc('preview.scale')} value={`${biomeFactor}`} onChange={v => setBiomeFactor(Number(v))} />
|
||||
<BtnInput label={loc('preview.depth')} value={`${biomeOffset}`} onChange={v => setBiomeOffset(Number(v))} />
|
||||
</>}
|
||||
</BtnMenu>
|
||||
<Btn icon="sync" onClick={() => setSeed(hexId())} />
|
||||
<Btn icon="sync" onClick={() => setSeed(randomSeed())} />
|
||||
</div>
|
||||
<canvas ref={canvas} width="200" height={data.height}></canvas>
|
||||
<canvas ref={canvas} width={size} height={size}></canvas>
|
||||
</>
|
||||
}
|
||||
|
||||
@@ -1,3 +1,14 @@
|
||||
import type { DataModel } from '@mcschema/core'
|
||||
import type { VersionId } from '../../Schemas'
|
||||
|
||||
export * from './BiomeSourcePreview'
|
||||
export * from './DecoratorPreview'
|
||||
export * from './NoiseSettingsPreview'
|
||||
|
||||
export type PreviewProps = {
|
||||
lang: string,
|
||||
model: DataModel,
|
||||
data: any,
|
||||
shown: boolean,
|
||||
version: VersionId,
|
||||
}
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
export * from './useCanvas'
|
||||
export * from './useFocus'
|
||||
export * from './useModel'
|
||||
export * from './useOnDrag'
|
||||
export * from './useOnHover'
|
||||
|
||||
89
src/app/hooks/useCanvas.ts
Normal file
89
src/app/hooks/useCanvas.ts
Normal file
@@ -0,0 +1,89 @@
|
||||
import type { Inputs } from 'preact/hooks'
|
||||
import { useEffect, useRef } from 'preact/hooks'
|
||||
|
||||
type Vec2 = [number, number]
|
||||
|
||||
export function useCanvas({ size, draw, onDrag, onHover, onLeave }: {
|
||||
size: () => Vec2,
|
||||
draw: (img: ImageData) => Promise<unknown>,
|
||||
onDrag?: (dx: number, dy: number) => Promise<unknown>,
|
||||
onHover?: (x: number, y: number) => unknown,
|
||||
onLeave?: () => unknown,
|
||||
}, inputs?: Inputs) {
|
||||
const canvas = useRef<HTMLCanvasElement>(null)
|
||||
|
||||
const dragStart = useRef<Vec2 | undefined>()
|
||||
const dragRequest = useRef<number>()
|
||||
const dragPending = useRef<Vec2>([0, 0])
|
||||
const dragBusy = useRef(false)
|
||||
|
||||
useEffect(() => {
|
||||
const onMouseDown = (e: MouseEvent) => {
|
||||
dragStart.current = [e.offsetX, e.offsetY]
|
||||
}
|
||||
const onMouseMove = (e: MouseEvent) => {
|
||||
if (dragStart.current === undefined) {
|
||||
const x = e.offsetX / canvas.current.clientWidth
|
||||
const y = e.offsetY / canvas.current.clientHeight
|
||||
onHover?.(x, y)
|
||||
return
|
||||
}
|
||||
if (!onDrag) return
|
||||
const dx = e.offsetX - dragStart.current[0]
|
||||
const dy = e.offsetY - dragStart.current[1]
|
||||
if (!(dx === 0 && dy === 0)) {
|
||||
dragPending.current = [dragPending.current[0] + dx, dragPending.current[1] + dy]
|
||||
if (!dragBusy.current) {
|
||||
cancelAnimationFrame(dragRequest.current)
|
||||
dragRequest.current = requestAnimationFrame(async () => {
|
||||
dragBusy.current = true
|
||||
const dx = dragPending.current[0] / canvas.current.clientWidth
|
||||
const dy = dragPending.current[1] / canvas.current.clientHeight
|
||||
dragPending.current = [0, 0]
|
||||
await onDrag?.(dx, dy)
|
||||
dragBusy.current = false
|
||||
})
|
||||
}
|
||||
}
|
||||
dragStart.current = [e.offsetX, e.offsetY]
|
||||
}
|
||||
const onMouseUp = () => {
|
||||
dragStart.current = undefined
|
||||
}
|
||||
const onMouseLeave = () => {
|
||||
onLeave?.()
|
||||
}
|
||||
|
||||
canvas.current.addEventListener('mousedown', onMouseDown)
|
||||
canvas.current.addEventListener('mousemove', onMouseMove)
|
||||
canvas.current.addEventListener('mouseleave', onMouseLeave)
|
||||
document.body.addEventListener('mouseup', onMouseUp)
|
||||
|
||||
return () => {
|
||||
canvas.current.removeEventListener('mousedown', onMouseDown)
|
||||
canvas.current.removeEventListener('mousemove', onMouseMove)
|
||||
canvas.current.removeEventListener('mouseleave', onMouseLeave)
|
||||
document.body.removeEventListener('mouseup', onMouseUp)
|
||||
}
|
||||
}, [...inputs ?? [], canvas.current])
|
||||
|
||||
const redraw = useRef<() => Promise<unknown>>()
|
||||
const redrawCount = useRef(0)
|
||||
redraw.current = async () => {
|
||||
const ctx = canvas.current.getContext('2d')!
|
||||
const s = size()
|
||||
canvas.current.width = s[0]
|
||||
canvas.current.height = s[1]
|
||||
const img = ctx.getImageData(0, 0, s[0], s[1])
|
||||
const ownCount = redrawCount.current += 1
|
||||
await draw(img)
|
||||
if (ownCount === redrawCount.current) {
|
||||
ctx.putImageData(img, 0, 0)
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
canvas,
|
||||
redraw: redraw.current,
|
||||
}
|
||||
}
|
||||
@@ -1,41 +0,0 @@
|
||||
import { useEffect, useRef } from 'preact/hooks'
|
||||
|
||||
export function useOnDrag(element: HTMLElement, drag: (dx: number, dy: number) => unknown) {
|
||||
if (!element) return
|
||||
|
||||
const request = useRef<number>()
|
||||
const dragStart = useRef<[number, number] | undefined>()
|
||||
const pending = useRef<[number, number]>([0, 0])
|
||||
|
||||
useEffect(() => {
|
||||
const onMouseDown = (e: MouseEvent) => {
|
||||
dragStart.current = [e.offsetX, e.offsetY]
|
||||
}
|
||||
const onMouseMove = (e: MouseEvent) => {
|
||||
if (dragStart.current === undefined) return
|
||||
const dx = e.offsetX - dragStart.current[0]
|
||||
const dy = e.offsetY - dragStart.current[1]
|
||||
if (!(dx === 0 && dy === 0)) {
|
||||
cancelAnimationFrame(request.current)
|
||||
pending.current = [pending.current[0] + dx, pending.current[1] + dy]
|
||||
request.current = requestAnimationFrame(() => {
|
||||
drag(...pending.current)
|
||||
pending.current = [0, 0]
|
||||
})
|
||||
}
|
||||
dragStart.current = [e.offsetX, e.offsetY]
|
||||
}
|
||||
const onMouseUp = (_e: MouseEvent) => {
|
||||
dragStart.current = undefined
|
||||
}
|
||||
|
||||
element.addEventListener('mousedown', onMouseDown)
|
||||
element.addEventListener('mousemove', onMouseMove)
|
||||
document.body.addEventListener('mouseup', onMouseUp)
|
||||
return () => {
|
||||
element.removeEventListener('mousedown', onMouseDown)
|
||||
element.removeEventListener('mousemove', onMouseMove)
|
||||
document.body.removeEventListener('mouseup', onMouseUp)
|
||||
}
|
||||
}, [element])
|
||||
}
|
||||
@@ -1,21 +0,0 @@
|
||||
import { useEffect } from 'preact/hooks'
|
||||
|
||||
export function useOnHover(element: HTMLElement, hover: (x: number | undefined, y: number | undefined) => unknown) {
|
||||
if (!element) return
|
||||
|
||||
const onMouseMove = (e: MouseEvent) => {
|
||||
hover(e.offsetX, e.offsetY)
|
||||
}
|
||||
const onMouseLeave = () => {
|
||||
hover(undefined, undefined)
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
element.addEventListener('mousemove', onMouseMove)
|
||||
element.addEventListener('mouseleave', onMouseLeave)
|
||||
return () => {
|
||||
element.removeEventListener('mousemove', onMouseMove)
|
||||
element.removeEventListener('mouseleave', onMouseLeave)
|
||||
}
|
||||
}, [element])
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
import type { DataModel } from '@mcschema/core'
|
||||
import { Path } from '@mcschema/core'
|
||||
import { getCurrentUrl } from 'preact-router'
|
||||
import { useEffect, useErrorBoundary, useState } from 'preact/hooks'
|
||||
import { useEffect, useErrorBoundary, useRef, useState } from 'preact/hooks'
|
||||
import config from '../../config.json'
|
||||
import { Analytics } from '../Analytics'
|
||||
import { Ad, Btn, BtnInput, BtnMenu, ErrorPanel, HasPreview, Octicon, PreviewPanel, SourcePanel, Tree } from '../components'
|
||||
@@ -13,7 +14,7 @@ import { getGenerator } from '../Utils'
|
||||
|
||||
type GeneratorProps = {
|
||||
lang: string,
|
||||
changeTitle: (title: string, versions?: string[]) => unknown,
|
||||
changeTitle: (title: string, versions?: VersionId[]) => unknown,
|
||||
version: VersionId,
|
||||
onChangeVersion: (version: VersionId) => unknown,
|
||||
default?: true,
|
||||
@@ -110,6 +111,13 @@ export function Generator({ lang, changeTitle, version, onChangeVersion }: Gener
|
||||
const loadPreset = (id: string) => {
|
||||
Analytics.generatorEvent('load-preset', id)
|
||||
fetchPreset(version, gen.path ?? gen.id, id).then(preset => {
|
||||
const seed = model?.get(new Path(['generator', 'seed']))
|
||||
if (preset?.generator?.seed !== undefined && seed !== undefined) {
|
||||
preset.generator.seed = seed
|
||||
if (preset.generator.biome_source?.seed !== undefined) {
|
||||
preset.generator.biome_source.seed = seed
|
||||
}
|
||||
}
|
||||
model?.reset(preset, false)
|
||||
})
|
||||
}
|
||||
@@ -140,6 +148,16 @@ export function Generator({ lang, changeTitle, version, onChangeVersion }: Gener
|
||||
setImport(0)
|
||||
}
|
||||
|
||||
const [copyActive, setCopyActive] = useState(false)
|
||||
const copyTimeout = useRef<number | undefined>(undefined)
|
||||
const copySuccess = () => {
|
||||
setCopyActive(true)
|
||||
if (copyTimeout.current !== undefined) clearTimeout(copyTimeout.current)
|
||||
copyTimeout.current = setTimeout(() => {
|
||||
setCopyActive(false)
|
||||
}, 2000)
|
||||
}
|
||||
|
||||
const [previewShown, setPreviewShown] = useState(false)
|
||||
const hasPreview = HasPreview.includes(gen.id)
|
||||
if (previewShown && !hasPreview) setPreviewShown(false)
|
||||
@@ -185,8 +203,8 @@ export function Generator({ lang, changeTitle, version, onChangeVersion }: Gener
|
||||
<div class={`popup-action action-download${sourceShown ? ' shown' : ''}`} onClick={downloadSource}>
|
||||
{Octicon.download}
|
||||
</div>
|
||||
<div class={`popup-action action-copy${sourceShown ? ' shown' : ''}`} onClick={copySource}>
|
||||
{Octicon.clippy}
|
||||
<div class={`popup-action action-copy${sourceShown ? ' shown' : ''}${copyActive ? ' active' : ''}`} onClick={copySource}>
|
||||
{copyActive ? Octicon.check : Octicon.clippy}
|
||||
</div>
|
||||
<div class={'popup-action action-code shown'} onClick={toggleSource}>
|
||||
{sourceShown ? Octicon.chevron_right : Octicon.code}
|
||||
@@ -196,7 +214,7 @@ export function Generator({ lang, changeTitle, version, onChangeVersion }: Gener
|
||||
<PreviewPanel {...{lang, model, version, id: gen.id}} shown={previewShown} onError={setError} />
|
||||
</div>
|
||||
<div class={`popup-source${sourceShown ? ' shown' : ''}`}>
|
||||
<SourcePanel {...{lang, model, blockStates, doCopy, doDownload, doImport}} name={gen.schema ?? 'data'} onError={setError} />
|
||||
<SourcePanel {...{lang, model, blockStates, doCopy, doDownload, doImport}} name={gen.schema ?? 'data'} copySuccess={copySuccess} onError={setError} />
|
||||
</div>
|
||||
</>
|
||||
}
|
||||
|
||||
@@ -1,62 +1,39 @@
|
||||
import { stringToColor } from '../Utils'
|
||||
import { NormalNoise } from './noise/NormalNoise'
|
||||
import type { BiomeSource, Climate, NoiseOctaves } from 'deepslate'
|
||||
import { FixedBiome, MultiNoise, NoiseGeneratorSettings, NoiseSampler, NormalNoise, Random } from 'deepslate'
|
||||
import { fetchPreset } from '../DataFetcher'
|
||||
import type { VersionId } from '../Schemas'
|
||||
import { deepClone, deepEqual, square, stringToColor } from '../Utils'
|
||||
|
||||
type BiomeColors =Record<string, number[]>
|
||||
type BiomeColors = Record<string, number[]>
|
||||
type BiomeSourceOptions = {
|
||||
octaves: NoiseOctaves,
|
||||
biomeColors: BiomeColors,
|
||||
offset: [number, number],
|
||||
scale: number,
|
||||
res: number,
|
||||
seed: string,
|
||||
seed: bigint,
|
||||
version: VersionId,
|
||||
}
|
||||
|
||||
const NoiseMaps = ['altitude', 'temperature', 'humidity', 'weirdness']
|
||||
let cacheState: any
|
||||
let biomeSourceCache: BiomeSource
|
||||
let climateSamplerCache: Climate.Sampler
|
||||
|
||||
export function biomeSource(state: any, img: ImageData, options: BiomeSourceOptions) {
|
||||
switch (state?.type?.replace(/^minecraft:/, '')) {
|
||||
case 'multi_noise': return multiNoise(state, img, options)
|
||||
case 'fixed': return fixed(state, img, options)
|
||||
case 'checkerboard': return checkerboard(state, img, options)
|
||||
}
|
||||
}
|
||||
|
||||
function fixed(state: any, img: ImageData, options: BiomeSourceOptions) {
|
||||
const data = img.data
|
||||
const color = getBiomeColor(state.biome, options.biomeColors)
|
||||
const row = img.width * 4 / options.res
|
||||
const col = 4 / options.res
|
||||
for (let x = 0; x < 200; x += options.res) {
|
||||
for (let y = 0; y < 200; y += options.res) {
|
||||
const i = y * row + x * col
|
||||
data[i] = color[0]
|
||||
data[i + 1] = color[1]
|
||||
data[i + 2] = color[2]
|
||||
data[i + 3] = 255
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function checkerboard(state: any, img: ImageData, options: BiomeSourceOptions) {
|
||||
const biomeColorCache: BiomeColors = {}
|
||||
state.biomes?.forEach((b: string) => {
|
||||
biomeColorCache[b] = getBiomeColor(b, options.biomeColors)
|
||||
})
|
||||
export async function biomeMap(state: any, img: ImageData, options: BiomeSourceOptions) {
|
||||
const { biomeSource, climateSampler } = await getCached(state, options)
|
||||
|
||||
const data = img.data
|
||||
const ox = -options.offset[0] - 100 + options.res / 2
|
||||
const oy = -options.offset[1] - 100 + options.res / 2
|
||||
const oz = -options.offset[1] - 100 + options.res / 2
|
||||
const row = img.width * 4 / options.res
|
||||
const col = 4 / options.res
|
||||
const shift = (state.scale ?? 2) + 2
|
||||
const numBiomes = state.biomes?.length ?? 0
|
||||
for (let x = 0; x < 200; x += options.res) {
|
||||
for (let y = 0; y < 200; y += options.res) {
|
||||
const i = y * row + x * col
|
||||
const xx = (x + ox) * options.scale
|
||||
const yy = (y + oy) * options.scale
|
||||
const j = (((xx >> shift) + (yy >> shift)) % numBiomes + numBiomes) % numBiomes
|
||||
const b = state.biomes?.[j]
|
||||
const color = biomeColorCache[b] ?? [128, 128, 128]
|
||||
for (let z = 0; z < 200; z += options.res) {
|
||||
const i = z * row + x * col
|
||||
const worldX = (x + ox) * options.scale
|
||||
const worldZ = (z + oz) * options.scale
|
||||
const b = biomeSource.getBiome(worldX, 64, worldZ, climateSampler)
|
||||
const color = getBiomeColor(b, options.biomeColors)
|
||||
data[i] = color[0]
|
||||
data[i + 1] = color[1]
|
||||
data[i + 2] = color[2]
|
||||
@@ -65,61 +42,86 @@ function checkerboard(state: any, img: ImageData, options: BiomeSourceOptions) {
|
||||
}
|
||||
}
|
||||
|
||||
function multiNoise(state: any, img: ImageData, options: BiomeSourceOptions) {
|
||||
if (state.preset?.replace(/^minecraft:/, '') === 'nether') {
|
||||
state = NetherPreset
|
||||
}
|
||||
export async function getBiome(state: any, x: number, z: number, options: BiomeSourceOptions): Promise<string | undefined> {
|
||||
const { biomeSource, climateSampler } = await getCached(state, options)
|
||||
|
||||
const noise = NoiseMaps.map((id, i) => {
|
||||
const config = state[`${id}_noise`]
|
||||
return new NormalNoise(options.seed + i, config.firstOctave, config.amplitudes)
|
||||
})
|
||||
|
||||
const biomeColorCache: BiomeColors = {}
|
||||
state.biomes.forEach((b: any) => {
|
||||
biomeColorCache[b.biome] = getBiomeColor(b.biome, options.biomeColors)
|
||||
})
|
||||
|
||||
const data = img.data
|
||||
const ox = -options.offset[0] - 100 + options.res / 2
|
||||
const oy = -options.offset[1] - 100 + options.res / 2
|
||||
const row = img.width * 4 / options.res
|
||||
const col = 4 / options.res
|
||||
for (let x = 0; x < 200; x += options.res) {
|
||||
for (let y = 0; y < 200; y += options.res) {
|
||||
const i = y * row + x * col
|
||||
const xx = (x + ox) * options.scale
|
||||
const yy = (y + oy) * options.scale
|
||||
const b = closestBiome(noise, state.biomes, xx, yy)
|
||||
const color = biomeColorCache[b] ?? [128, 128, 128]
|
||||
data[i] = color[0]
|
||||
data[i + 1] = color[1]
|
||||
data[i + 2] = color[2]
|
||||
data[i + 3] = 255
|
||||
}
|
||||
}
|
||||
const [xx, zz] = toWorld([x, z], options)
|
||||
return biomeSource.getBiome(xx, 64, zz, climateSampler)
|
||||
}
|
||||
|
||||
export function getBiome(state: any, x: number, y: number, options: BiomeSourceOptions): string | undefined {
|
||||
const [xx, yy] = toWorld([x, y], options)
|
||||
async function getCached(state: any, options: BiomeSourceOptions): Promise<{ biomeSource: BiomeSource, climateSampler: Climate.Sampler }> {
|
||||
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,
|
||||
}
|
||||
}
|
||||
|
||||
async function getBiomeSource(state: any, options: BiomeSourceOptions): Promise<BiomeSource> {
|
||||
switch (state?.type?.replace(/^minecraft:/, '')) {
|
||||
case 'multi_noise':
|
||||
const noise = NoiseMaps.map((id, i) => {
|
||||
const config = state[`${id}_noise`]
|
||||
return new NormalNoise(options.seed + i, config.firstOctave, config.amplitudes)
|
||||
})
|
||||
return closestBiome(noise, state.biomes, xx, yy)
|
||||
case 'fixed': return state.biome
|
||||
case 'fixed':
|
||||
return new FixedBiome(state.biome as string)
|
||||
|
||||
case 'checkerboard':
|
||||
const shift = (state.scale ?? 2) + 2
|
||||
const numBiomes = state.biomes?.length ?? 0
|
||||
const j = (((xx >> shift) + (yy >> shift)) % numBiomes + numBiomes) % numBiomes
|
||||
return state.biomes?.[j]
|
||||
return {
|
||||
getBiome(x: number, _y: number, z: number) {
|
||||
const i = (((x >> shift) + (z >> shift)) % numBiomes + numBiomes) % numBiomes
|
||||
return (state.biomes?.[i] as string)
|
||||
},
|
||||
}
|
||||
|
||||
case 'multi_noise':
|
||||
switch(state.preset?.replace(/^minecraft:/, '')) {
|
||||
case 'nether':
|
||||
state = options.version === '1.18' ? NetherPreset18 : NetherPreset
|
||||
break
|
||||
case 'overworld':
|
||||
state = options.version === '1.18' ? await OverworldPreset18() : state
|
||||
break
|
||||
}
|
||||
if (options.version === '1.18') {
|
||||
return MultiNoise.fromJson(state)
|
||||
} else {
|
||||
const noise = ['altitude', 'temperature', 'humidity', 'weirdness']
|
||||
.map((id, i) => {
|
||||
const config = state[`${id}_noise`]
|
||||
return new NormalNoise(new Random(options.seed + BigInt(i)), config)
|
||||
})
|
||||
if (!Array.isArray(state.biomes) || state.biomes.length === 0) {
|
||||
return new FixedBiome('unknown')
|
||||
}
|
||||
return {
|
||||
getBiome(x: number, _y: number, z: number): string {
|
||||
const n = noise.map(n => n.sample(x, z, 0))
|
||||
let minDist = Infinity
|
||||
let minBiome = ''
|
||||
for (const { biome, parameters: p } of state.biomes) {
|
||||
const dist = square(p.altitude - n[0]) + square(p.temperature - n[1]) + square(p.humidity - n[2]) + square(p.weirdness - n[3]) + square(p.offset)
|
||||
if (dist < minDist) {
|
||||
minDist = dist
|
||||
minBiome = biome
|
||||
}
|
||||
}
|
||||
return minBiome
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
return undefined
|
||||
throw new Error('Unknown biome source')
|
||||
}
|
||||
|
||||
export function getBiomeColor(biome: string, biomeColors: BiomeColors) {
|
||||
function getBiomeColor(biome: string, biomeColors: BiomeColors) {
|
||||
if (!biome) {
|
||||
return [128, 128, 128]
|
||||
}
|
||||
@@ -130,29 +132,10 @@ export function getBiomeColor(biome: string, biomeColors: BiomeColors) {
|
||||
return color
|
||||
}
|
||||
|
||||
function toWorld([x, y]: [number, number], options: BiomeSourceOptions) {
|
||||
function toWorld([x, z]: [number, number], options: BiomeSourceOptions) {
|
||||
const xx = (x - options.offset[0] - 100 + options.res / 2) * options.scale
|
||||
const yy = (y - options.offset[1] - 100 + options.res / 2) * options.scale
|
||||
return [xx, yy]
|
||||
}
|
||||
|
||||
function closestBiome(noise: NormalNoise[], biomes: any[], x: number, y: number): string {
|
||||
if (!Array.isArray(biomes) || biomes.length === 0) return ''
|
||||
const n = noise.map(n => n.getValue(x, y, 0))
|
||||
let minDist = Infinity
|
||||
let minBiome = ''
|
||||
for (const b of biomes) {
|
||||
const dist = fitness(b.parameters, {altitude: n[0], temperature: n[1], humidity: n[2], weirdness: n[3], offset: 0})
|
||||
if (dist < minDist) {
|
||||
minDist = dist
|
||||
minBiome = b.biome
|
||||
}
|
||||
}
|
||||
return minBiome
|
||||
}
|
||||
|
||||
function fitness(a: any, b: any) {
|
||||
return (a.altitude - b.altitude) * (a.altitude - b.altitude) + (a.temperature - b.temperature) * (a.temperature - b.temperature) + (a.humidity - b.humidity) * (a.humidity - b.humidity) + (a.weirdness - b.weirdness) * (a.weirdness - b.weirdness) + (a.offset - b.offset) * (a.offset - b.offset)
|
||||
const zz = (z - options.offset[1] - 100 + options.res / 2) * options.scale
|
||||
return [xx, zz]
|
||||
}
|
||||
|
||||
const VanillaColors: Record<string, [number, number, number]> = {
|
||||
@@ -235,6 +218,21 @@ const VanillaColors: Record<string, [number, number, number]> = {
|
||||
'minecraft:wooded_badlands_plateau': [176,151,101],
|
||||
'minecraft:wooded_hills': [34,85,28],
|
||||
'minecraft:wooded_mountains': [80,112,80],
|
||||
'minecraft:snowy_slopes': [140, 195, 222],
|
||||
'minecraft:lofty_peaks': [196, 168, 193],
|
||||
'minecraft:snowcapped_peaks': [200, 198, 200],
|
||||
'minecraft:stony_peaks': [82, 92, 103],
|
||||
'minecraft:grove': [150, 150, 189],
|
||||
'minecraft:meadow': [169, 197, 80],
|
||||
'minecraft:lush_caves': [112, 255, 79],
|
||||
'minecraft:dripstone_caves': [140, 124, 0],
|
||||
}
|
||||
|
||||
const NetherPreset = {type:'minecraft:multi_noise',seed:0,altitude_noise:{firstOctave:-7,amplitudes:[1,1]},temperature_noise:{firstOctave:-7,amplitudes:[1,1]},humidity_noise:{firstOctave:-7,amplitudes:[1,1]},weirdness_noise:{firstOctave:-7,amplitudes:[1,1]},biomes:[{biome:'minecraft:nether_wastes',parameters:{altitude:0,temperature:0,humidity:0,weirdness:0,offset:0}},{biome:'minecraft:soul_sand_valley',parameters:{altitude:0,temperature:0,humidity:-0.5,weirdness:0,offset:0}},{biome:'minecraft:crimson_forest',parameters:{altitude:0,temperature:0.4,humidity:0,weirdness:0,offset:0}},{biome:'minecraft:warped_forest',parameters:{altitude:0,temperature:0,humidity:0.5,weirdness:0,offset:0.375}},{biome:'minecraft:basalt_deltas',parameters:{altitude:0,temperature:-0.5,humidity:0,weirdness:0,offset:0.175}}]}
|
||||
|
||||
const NetherPreset18 = {type:'minecraft:multi_noise',biomes:[{biome:'minecraft:nether_wastes',parameters:{temperature:0,humidity:0,continentalness:0,erosion:0,depth:0,weirdness:0,offset:0}},{biome:'minecraft:soul_sand_valley',parameters:{temperature:0,humidity:-0.5,continentalness:0,erosion:0,depth:0,weirdness:0,offset:0}},{biome:'minecraft:crimson_forest',parameters:{temperature:0.4,humidity:0,continentalness:0,erosion:0,depth:0,weirdness:0,offset:0}},{biome:'minecraft:warped_forest',parameters:{temperature:0,humidity:0.5,continentalness:0,erosion:0,depth:0,weirdness:0,offset:0.375}},{biome:'minecraft:basalt_deltas',parameters:{temperature:-0.5,humidity:0,continentalness:0,erosion:0,depth:0,weirdness:0,offset:0.175}}]}
|
||||
|
||||
async function OverworldPreset18() {
|
||||
const overworld = await fetchPreset('1.18', 'dimension', 'overworld')
|
||||
return overworld.generator.biome_source
|
||||
}
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import seedrandom from 'seedrandom'
|
||||
import { PerlinNoise, Random } from 'deepslate'
|
||||
import type { VersionId } from '../Schemas'
|
||||
import { clamp, stringToColor } from '../Utils'
|
||||
import { PerlinNoise } from './noise/PerlinNoise'
|
||||
|
||||
type BlockPos = [number, number, number]
|
||||
type Placement = [BlockPos, number]
|
||||
@@ -9,10 +8,13 @@ type Placement = [BlockPos, number]
|
||||
type PlacementContext = {
|
||||
placements: Placement[],
|
||||
features: string[],
|
||||
random: seedrandom.prng,
|
||||
random: Random,
|
||||
biomeInfoNoise: PerlinNoise,
|
||||
seaLevel: number,
|
||||
version: VersionId,
|
||||
nextFloat(): number,
|
||||
nextInt(max: number): number,
|
||||
sampleInt(provider: any): number,
|
||||
}
|
||||
|
||||
const terrain = [50, 50, 51, 51, 52, 52, 53, 54, 56, 57, 57, 58, 58, 59, 60, 60, 60, 59, 59, 59, 60, 61, 61, 62, 63, 63, 64, 64, 64, 65, 65, 66, 66, 65, 65, 66, 66, 67, 67, 67, 68, 69, 71, 73, 74, 76, 79, 80, 81, 81, 82, 83, 83, 82, 82, 81, 81, 80, 80, 80, 81, 81, 82, 82]
|
||||
@@ -28,18 +30,21 @@ const featureColors = [
|
||||
|
||||
export type DecoratorOptions = {
|
||||
size: [number, number, number],
|
||||
seed: string,
|
||||
seed: bigint,
|
||||
version: VersionId,
|
||||
}
|
||||
export function decorator(state: any, img: ImageData, options: DecoratorOptions) {
|
||||
const random = seedrandom(options.seed)
|
||||
const random = new Random(options.seed)
|
||||
const ctx: PlacementContext = {
|
||||
placements: [],
|
||||
features: [],
|
||||
random,
|
||||
biomeInfoNoise: new PerlinNoise(options.seed + 'frwynup', 0, [1]),
|
||||
biomeInfoNoise: new PerlinNoise(random.fork(), 0, [1]),
|
||||
seaLevel: 63,
|
||||
version: options.version,
|
||||
nextFloat: () => random.nextFloat(),
|
||||
nextInt: (max: number) => random.nextInt(max),
|
||||
sampleInt(value) { return sampleInt(value, this) },
|
||||
}
|
||||
|
||||
for (let x = 0; x < options.size[0] / 16; x += 1) {
|
||||
@@ -80,20 +85,16 @@ function decorateY(pos: BlockPos, y: number): BlockPos[] {
|
||||
return [[ pos[0], y, pos[2] ]]
|
||||
}
|
||||
|
||||
function nextInt(max: number, ctx: PlacementContext): number {
|
||||
return Math.floor(ctx.random() * max)
|
||||
}
|
||||
|
||||
function sampleInt(value: any, ctx: PlacementContext): number {
|
||||
if (typeof value === 'number') {
|
||||
return value
|
||||
} else if (value.base) {
|
||||
return value.base ?? 1 + nextInt(1 + (value.spread ?? 0), ctx)
|
||||
return value.base ?? 1 + ctx.nextInt(1 + (value.spread ?? 0))
|
||||
} else {
|
||||
switch (normalize(value.type)) {
|
||||
case 'constant': return value.value
|
||||
case 'uniform': return value.value.min_inclusive + nextInt(value.value.max_inclusive - value.value.min_inclusive + 1, ctx)
|
||||
case 'biased_to_bottom': return value.value.min_inclusive + nextInt(nextInt(value.value.max_inclusive - value.value.min_inclusive + 1, ctx) + 1, ctx)
|
||||
case 'uniform': return value.value.min_inclusive + ctx.nextInt(value.value.max_inclusive - value.value.min_inclusive + 1)
|
||||
case 'biased_to_bottom': return value.value.min_inclusive + ctx.nextInt(ctx.nextInt(value.value.max_inclusive - value.value.min_inclusive + 1) + 1)
|
||||
case 'clamped': return Math.max(value.value.min_inclusive, Math.min(value.value.max_inclusive, sampleInt(value.value.source, ctx)))
|
||||
}
|
||||
return 1
|
||||
@@ -138,12 +139,12 @@ const Features: {
|
||||
positions.forEach(p => getPlacements(p, config?.feature, ctx))
|
||||
},
|
||||
random_boolean_selector: (config, pos, ctx) => {
|
||||
const feature = ctx.random() < 0.5 ? config?.feature_true : config?.feature_false
|
||||
const feature = ctx.nextFloat() < 0.5 ? config?.feature_true : config?.feature_false
|
||||
getPlacements(pos, feature, ctx)
|
||||
},
|
||||
random_selector: (config, pos, ctx) => {
|
||||
for (const f of config?.features ?? []) {
|
||||
if (ctx.random() < (f?.chance ?? 0)) {
|
||||
if (ctx.nextFloat() < (f?.chance ?? 0)) {
|
||||
getPlacements(pos, f.feature, ctx)
|
||||
return
|
||||
}
|
||||
@@ -151,7 +152,7 @@ const Features: {
|
||||
getPlacements(pos, config?.default, ctx)
|
||||
},
|
||||
simple_random_selector: (config, pos, ctx) => {
|
||||
const feature = config?.features?.[nextInt(config?.features?.length ?? 0, ctx)]
|
||||
const feature = config?.features?.[ctx.nextInt(config?.features?.length ?? 0)]
|
||||
getPlacements(pos, feature, ctx)
|
||||
},
|
||||
}
|
||||
@@ -160,42 +161,42 @@ const Decorators: {
|
||||
[key: string]: (config: any, pos: BlockPos, ctx: PlacementContext) => BlockPos[],
|
||||
} = {
|
||||
chance: (config, pos, ctx) => {
|
||||
return ctx.random() < 1 / (config?.chance ?? 1) ? [pos] : []
|
||||
return ctx.nextFloat() < 1 / (config?.chance ?? 1) ? [pos] : []
|
||||
},
|
||||
count: (config, pos, ctx) => {
|
||||
return new Array(sampleInt(config?.count ?? 1, ctx)).fill(pos)
|
||||
return new Array(ctx.sampleInt(config?.count ?? 1)).fill(pos)
|
||||
},
|
||||
count_extra: (config, pos, ctx) => {
|
||||
let count = config?.count ?? 1
|
||||
if (ctx.random() < config.extra_chance ?? 0){
|
||||
if (ctx.nextFloat() < config.extra_chance ?? 0){
|
||||
count += config.extra_count ?? 0
|
||||
}
|
||||
return new Array(count).fill(pos)
|
||||
},
|
||||
count_multilayer: (config, pos, ctx) => {
|
||||
return new Array(sampleInt(config?.count ?? 1, ctx)).fill(pos)
|
||||
return new Array(ctx.sampleInt(config?.count ?? 1)).fill(pos)
|
||||
.map(p => [
|
||||
p[0] + nextInt(16, ctx),
|
||||
p[0] + ctx.nextInt(16),
|
||||
p[1],
|
||||
p[2] + nextInt(16, ctx),
|
||||
p[2] + ctx.nextInt(16),
|
||||
])
|
||||
},
|
||||
count_noise: (config, pos, ctx) => {
|
||||
const noise = ctx.biomeInfoNoise.getValue(pos[0] / 200, 0, pos[2] / 200)
|
||||
const noise = ctx.biomeInfoNoise.sample(pos[0] / 200, 0, pos[2] / 200)
|
||||
const count = noise < config.noise_level ? config.below_noise : config.above_noise
|
||||
return new Array(count).fill(pos)
|
||||
},
|
||||
count_noise_biased: (config, pos, ctx) => {
|
||||
const factor = Math.max(1, config.noise_factor)
|
||||
const noise = ctx.biomeInfoNoise.getValue(pos[0] / factor, 0, pos[2] / factor)
|
||||
const noise = ctx.biomeInfoNoise.sample(pos[0] / factor, 0, pos[2] / factor)
|
||||
const count = Math.max(0, Math.ceil((noise + (config.noise_offset ?? 0)) * config.noise_to_count_ratio))
|
||||
return new Array(count).fill(pos)
|
||||
},
|
||||
dark_oak_tree: (_config, pos, ctx) => {
|
||||
return [...new Array(16)].map((_, i) => {
|
||||
const x = Math.floor(i / 4) * 4 + 1 + nextInt(3, ctx) + pos[0]
|
||||
const x = Math.floor(i / 4) * 4 + 1 + ctx.nextInt(3) + pos[0]
|
||||
const y = Math.max(ctx.seaLevel, terrain[clamp(0, 63, x)])
|
||||
const z = Math.floor(i % 4) * 4 + 1 + nextInt(3, ctx) + pos[2]
|
||||
const z = Math.floor(i % 4) * 4 + 1 + ctx.nextInt(3) + pos[2]
|
||||
return [x, y, z]
|
||||
})
|
||||
},
|
||||
@@ -205,31 +206,31 @@ const Decorators: {
|
||||
})
|
||||
},
|
||||
depth_average: (config, pos, ctx) => {
|
||||
const y = nextInt(config?.spread ?? 0, ctx) + nextInt(config?.spread ?? 0, ctx) - (config.spread ?? 0) + (config?.baseline ?? 0)
|
||||
const y = ctx.nextInt(config?.spread ?? 0) + ctx.nextInt(config?.spread ?? 0) - (config.spread ?? 0) + (config?.baseline ?? 0)
|
||||
return decorateY(pos, y)
|
||||
},
|
||||
emerald_ore: (_config, pos, ctx) => {
|
||||
const count = 3 + nextInt(6, ctx)
|
||||
const count = 3 + ctx.nextInt(6)
|
||||
return [...new Array(count)].map(() => [
|
||||
pos[0] + nextInt(16, ctx),
|
||||
4 + nextInt(28, ctx),
|
||||
pos[2] + nextInt(16, ctx),
|
||||
pos[0] + ctx.nextInt(16),
|
||||
4 + ctx.nextInt(28),
|
||||
pos[2] + ctx.nextInt(16),
|
||||
])
|
||||
},
|
||||
fire: (config, pos, ctx) => {
|
||||
const count = 1 + nextInt(nextInt(sampleInt(config?.count, ctx), ctx), ctx)
|
||||
const count = 1 + ctx.nextInt(ctx.nextInt(ctx.sampleInt(config?.count)))
|
||||
return [...new Array(count)].map(() => [
|
||||
pos[0] + nextInt(16, ctx),
|
||||
nextInt(128, ctx),
|
||||
pos[2] + nextInt(16, ctx),
|
||||
pos[0] + ctx.nextInt(16),
|
||||
ctx.nextInt(128),
|
||||
pos[2] + ctx.nextInt(16),
|
||||
])
|
||||
},
|
||||
glowstone: (config, pos, ctx) => {
|
||||
const count = nextInt(1 + nextInt(sampleInt(config?.count, ctx), ctx), ctx)
|
||||
const count = ctx.nextInt(1 + ctx.nextInt(ctx.sampleInt(config?.count)))
|
||||
return [...new Array(count)].map(() => [
|
||||
pos[0] + nextInt(16, ctx),
|
||||
nextInt(128, ctx),
|
||||
pos[2] + nextInt(16, ctx),
|
||||
pos[0] + ctx.nextInt(16),
|
||||
ctx.nextInt(128),
|
||||
pos[2] + ctx.nextInt(16),
|
||||
])
|
||||
},
|
||||
heightmap: (_config, pos, ctx) => {
|
||||
@@ -238,7 +239,7 @@ const Decorators: {
|
||||
},
|
||||
heightmap_spread_double: (_config, pos, ctx) => {
|
||||
const y = Math.max(ctx.seaLevel, terrain[clamp(0, 63, pos[0])])
|
||||
return decorateY(pos, nextInt(y * 2, ctx))
|
||||
return decorateY(pos, ctx.nextInt(y * 2))
|
||||
},
|
||||
heightmap_world_surface: (_config, pos, ctx) => {
|
||||
const y = Math.max(ctx.seaLevel, terrain[clamp(0, 63, pos[0])])
|
||||
@@ -246,17 +247,17 @@ const Decorators: {
|
||||
},
|
||||
iceberg: (_config, pos, ctx) => {
|
||||
return [[
|
||||
pos[0] + 4 + nextInt(8, ctx),
|
||||
pos[0] + 4 + ctx.nextInt(8),
|
||||
pos[1],
|
||||
pos[2] + 4 + nextInt(8, ctx),
|
||||
pos[2] + 4 + ctx.nextInt(8),
|
||||
]]
|
||||
},
|
||||
lava_lake: (config, pos, ctx) => {
|
||||
if (nextInt((config.chance ?? 1) / 10, ctx) === 0) {
|
||||
const y = nextInt(nextInt(256 - 8, ctx) + 8, ctx)
|
||||
if (y < ctx.seaLevel || nextInt((config?.chance ?? 1) / 8, ctx) == 0) {
|
||||
const x = nextInt(16, ctx) + pos[0]
|
||||
const z = nextInt(16, ctx) + pos[2]
|
||||
if (ctx.nextInt((config.chance ?? 1) / 10) === 0) {
|
||||
const y = ctx.nextInt(ctx.nextInt(256 - 8) + 8)
|
||||
if (y < ctx.seaLevel || ctx.nextInt((config?.chance ?? 1) / 8) == 0) {
|
||||
const x = ctx.nextInt(16) + pos[0]
|
||||
const z = ctx.nextInt(16) + pos[2]
|
||||
return [[x, y, z]]
|
||||
}
|
||||
}
|
||||
@@ -266,19 +267,19 @@ const Decorators: {
|
||||
return [pos]
|
||||
},
|
||||
range: (config, pos, ctx) => {
|
||||
const y = nextInt((config?.maximum ?? 1) - (config?.top_offset ?? 0), ctx) + (config?.bottom_offset ?? 0)
|
||||
const y = ctx.nextInt((config?.maximum ?? 1) - (config?.top_offset ?? 0)) + (config?.bottom_offset ?? 0)
|
||||
return decorateY(pos, y)
|
||||
},
|
||||
range_biased: (config, pos, ctx) => {
|
||||
const y = nextInt(nextInt((config?.maximum ?? 1) - (config?.top_offset ?? 0), ctx) + (config?.bottom_offset ?? 0), ctx)
|
||||
const y = ctx.nextInt(ctx.nextInt((config?.maximum ?? 1) - (config?.top_offset ?? 0)) + (config?.bottom_offset ?? 0))
|
||||
return decorateY(pos, y)
|
||||
},
|
||||
range_very_biased: (config, pos, ctx) => {
|
||||
const y = nextInt(nextInt(nextInt((config?.maximum ?? 1) - (config?.top_offset ?? 0), ctx) + (config?.bottom_offset ?? 0), ctx) + (config?.bottom_offset ?? 0), ctx)
|
||||
const y = ctx.nextInt(ctx.nextInt(ctx.nextInt((config?.maximum ?? 1) - (config?.top_offset ?? 0)) + (config?.bottom_offset ?? 0)) + (config?.bottom_offset ?? 0))
|
||||
return decorateY(pos, y)
|
||||
},
|
||||
spread_32_above: (_config, pos, ctx) => {
|
||||
const y = nextInt(pos[1] + 32, ctx)
|
||||
const y = ctx.nextInt(pos[1] + 32)
|
||||
return decorateY(pos, y)
|
||||
},
|
||||
top_solid_heightmap: (_config, pos) => {
|
||||
@@ -286,22 +287,28 @@ const Decorators: {
|
||||
return decorateY(pos, y)
|
||||
},
|
||||
magma: (_config, pos, ctx) => {
|
||||
const y = nextInt(pos[1] + 32, ctx)
|
||||
const y = ctx.nextInt(pos[1] + 32)
|
||||
return decorateY(pos, y)
|
||||
},
|
||||
square: (_config, pos, ctx) => {
|
||||
return [[
|
||||
pos[0] + nextInt(16, ctx),
|
||||
pos[0] + ctx.nextInt(16),
|
||||
pos[1],
|
||||
pos[2] + nextInt(16, ctx),
|
||||
pos[2] + ctx.nextInt(16),
|
||||
]]
|
||||
},
|
||||
surface_relative_threshold: (config, pos) => {
|
||||
const height = terrain[clamp(0, 63, pos[0])]
|
||||
const min = height + (config?.min_inclusive ?? -Infinity)
|
||||
const max = height + (config?.max_inclusive ?? Infinity)
|
||||
return (pos[1] < min || pos[1] > max) ? [pos] : []
|
||||
},
|
||||
water_lake: (config, pos, ctx) => {
|
||||
if (nextInt(config.chance ?? 1, ctx) === 0) {
|
||||
if (ctx.nextInt(config.chance ?? 1) === 0) {
|
||||
return [[
|
||||
pos[0] + nextInt(16, ctx),
|
||||
nextInt(256, ctx),
|
||||
pos[2] + nextInt(16, ctx),
|
||||
pos[0] + ctx.nextInt(16),
|
||||
ctx.nextInt(256),
|
||||
pos[2] + ctx.nextInt(16),
|
||||
]]
|
||||
}
|
||||
return []
|
||||
|
||||
@@ -1,21 +1,64 @@
|
||||
import { NoiseChunkGenerator } from './noise/NoiseChunkGenerator'
|
||||
import type { BlockPos, BlockState } from 'deepslate'
|
||||
import { Chunk, ChunkPos, FixedBiome, NoiseChunkGenerator, NoiseGeneratorSettings } from 'deepslate'
|
||||
import type { VersionId } from '../Schemas'
|
||||
import { checkVersion } from '../Schemas'
|
||||
import { deepClone, deepEqual } from '../Utils'
|
||||
import { NoiseChunkGenerator as OldNoiseChunkGenerator } from './noise/NoiseChunkGenerator'
|
||||
|
||||
export type NoiseSettingsOptions = {
|
||||
biomeScale: number,
|
||||
biomeDepth: number,
|
||||
biomeFactor: number,
|
||||
biomeOffset: number,
|
||||
biomePeaks: number,
|
||||
offset: number,
|
||||
width: number,
|
||||
seed: string,
|
||||
seed: bigint,
|
||||
version: VersionId,
|
||||
}
|
||||
|
||||
const Z = 0
|
||||
|
||||
const colors: Record<string, [number, number, number]> = {
|
||||
'minecraft:air': [150, 160, 170],
|
||||
'minecraft:water': [20, 80, 170],
|
||||
'minecraft:lava': [200, 100, 0],
|
||||
'minecraft:stone': [50, 50, 50],
|
||||
'minecraft:netherrack': [100, 40, 40],
|
||||
'minecraft:end_stone': [200, 200, 140],
|
||||
}
|
||||
|
||||
let cacheState: any
|
||||
let generatorCache: NoiseChunkGenerator
|
||||
let chunkCache: Chunk[] = []
|
||||
|
||||
export function noiseSettings(state: any, img: ImageData, options: NoiseSettingsOptions) {
|
||||
const generator = new NoiseChunkGenerator(options.seed)
|
||||
generator.reset(state, options.biomeDepth, options.biomeScale, options.offset, 200)
|
||||
if (checkVersion(options.version, '1.18')) {
|
||||
const { settings, generator } = getCached(state, options)
|
||||
|
||||
const slice = new LevelSlice(-options.offset, options.width, settings.noise.minY, settings.noise.height)
|
||||
slice.fill(generator)
|
||||
|
||||
const data = img.data
|
||||
for (let x = 0; x < options.width; x += 1) {
|
||||
for (let y = 0; y < settings.noise.height; y += 1) {
|
||||
const i = x * 4 + (settings.noise.height-y-1) * 4 * img.width
|
||||
const state = slice.getBlockState([x - options.offset, y, Z])
|
||||
const color = colors[state.getName()] ?? [0, 0, 0]
|
||||
data[i] = color[0]
|
||||
data[i + 1] = color[1]
|
||||
data[i + 2] = color[2]
|
||||
data[i + 3] = 255
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
const generator = new OldNoiseChunkGenerator(options.seed)
|
||||
generator.reset(state.noise, options.biomeOffset, options.biomeFactor, options.offset, 200)
|
||||
const data = img.data
|
||||
const row = img.width * 4
|
||||
for (let x = 0; x < options.width; x += 1) {
|
||||
const noise = generator.iterateNoiseColumn(x - options.offset).reverse()
|
||||
for (let y = 0; y < state.height; y += 1) {
|
||||
for (let y = 0; y < state.noise.height; y += 1) {
|
||||
const i = y * row + x * 4
|
||||
const color = getColor(noise, y)
|
||||
data[i] = color
|
||||
@@ -26,6 +69,26 @@ export function noiseSettings(state: any, img: ImageData, options: NoiseSettings
|
||||
}
|
||||
}
|
||||
|
||||
function getCached(state: unknown, options: NoiseSettingsOptions) {
|
||||
const settings = NoiseGeneratorSettings.fromJson(state)
|
||||
// Temporary fix for slides
|
||||
settings.noise.bottomSlide.target *= 128
|
||||
settings.noise.topSlide.target *= 128
|
||||
const shape = { factor: options.biomeFactor, offset: options.biomeOffset, peaks: options.biomePeaks, nearWater: false }
|
||||
|
||||
const newState = [state, shape, `${options.seed}`]
|
||||
if (!deepEqual(newState, cacheState)) {
|
||||
cacheState = deepClone(newState)
|
||||
chunkCache = []
|
||||
const biomeSource = new FixedBiome('unknown')
|
||||
generatorCache = new NoiseChunkGenerator(options.seed, biomeSource, settings, shape)
|
||||
}
|
||||
return {
|
||||
settings,
|
||||
generator: generatorCache,
|
||||
}
|
||||
}
|
||||
|
||||
function getColor(noise: number[], y: number): number {
|
||||
if (noise[y] > 0) {
|
||||
return 0
|
||||
@@ -35,3 +98,42 @@ function getColor(noise: number[], y: number): number {
|
||||
}
|
||||
return 255
|
||||
}
|
||||
|
||||
class LevelSlice {
|
||||
private readonly chunks: Chunk[]
|
||||
private readonly filled: boolean[]
|
||||
|
||||
constructor(
|
||||
private readonly minX: number,
|
||||
width: number,
|
||||
minY: number,
|
||||
height: number,
|
||||
) {
|
||||
this.filled = []
|
||||
this.chunks = [...Array(Math.ceil(width / 16) + 1)]
|
||||
.map((_, i) => {
|
||||
const x = (minX >> 4) + i
|
||||
const cached = chunkCache.find(c => c.pos[0] === x)
|
||||
if (cached) {
|
||||
this.filled[i] = true
|
||||
return cached
|
||||
}
|
||||
return new Chunk(minY, height, ChunkPos.create(x, Z >> 4))
|
||||
})
|
||||
}
|
||||
|
||||
public fill(generator: NoiseChunkGenerator) {
|
||||
this.chunks.forEach((chunk, i) => {
|
||||
if (!this.filled[i]) {
|
||||
generator.fill(chunk)
|
||||
this.filled[i] = true
|
||||
chunkCache.push(chunk)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
public getBlockState(pos: BlockPos): BlockState {
|
||||
const chunkIndex = (pos[0] >> 4) - (this.minX >> 4)
|
||||
return this.chunks[chunkIndex].getBlockState(pos)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,79 +0,0 @@
|
||||
import type seedrandom from 'seedrandom'
|
||||
import { lerp3, smoothstep } from '../../Utils'
|
||||
|
||||
export class ImprovedNoise {
|
||||
private static readonly GRADIENT = [[1, 1, 0], [-1, 1, 0], [1, -1, 0], [-1, -1, 0], [1, 0, 1], [-1, 0, 1], [1, 0, -1], [-1, 0, -1], [0, 1, 1], [0, -1, 1], [0, 1, -1], [0, -1, -1], [1, 1, 0], [0, -1, 1], [-1, 1, 0], [0, -1, -1]]
|
||||
private readonly p: number[]
|
||||
public readonly xo: number
|
||||
public readonly yo: number
|
||||
public readonly zo: number
|
||||
|
||||
constructor(random: seedrandom.prng) {
|
||||
this.xo = random() * 256
|
||||
this.yo = random() * 256
|
||||
this.zo = random() * 256
|
||||
this.p = Array(256)
|
||||
|
||||
for (let i = 0; i < 256; i += 1) {
|
||||
this.p[i] = i
|
||||
}
|
||||
for (let i = 0; i < 256; i += 1) {
|
||||
const n = random.int32() % (256 - i)
|
||||
const b = this.p[i]
|
||||
this.p[i] = this.p[i + n]
|
||||
this.p[i + n] = b
|
||||
}
|
||||
}
|
||||
|
||||
public noise(x: number, y: number, z: number, a: number, b: number) {
|
||||
const x2 = x + this.xo
|
||||
const y2 = y + this.yo
|
||||
const z2 = z + this.zo
|
||||
const x3 = Math.floor(x2)
|
||||
const y3 = Math.floor(y2)
|
||||
const z3 = Math.floor(z2)
|
||||
const x4 = x2 - x3
|
||||
const y4 = y2 - y3
|
||||
const z4 = z2 - z3
|
||||
const x5 = smoothstep(x4)
|
||||
const y5 = smoothstep(y4)
|
||||
const z5 = smoothstep(z4)
|
||||
|
||||
let y6 = 0
|
||||
if (a !== 0) {
|
||||
y6 = Math.floor(Math.min(b, y4) / a) * a
|
||||
}
|
||||
|
||||
return this.sampleAndLerp(x3, y3, z3, x4, y4 - y6, z4, x5, y5, z5)
|
||||
}
|
||||
|
||||
private gradDot(a: number, b: number, c: number, d: number) {
|
||||
const grad = ImprovedNoise.GRADIENT[a & 15]
|
||||
return grad[0] * b + grad[1] * c + grad[2] * d
|
||||
}
|
||||
|
||||
private P(i: number) {
|
||||
return this.p[i & 255] & 255
|
||||
}
|
||||
|
||||
public sampleAndLerp(a: number, b: number, c: number, d: number, e: number, f: number, g: number, h: number, i: number) {
|
||||
const j = this.P(a) + b
|
||||
const k = this.P(j) + c
|
||||
const l = this.P(j + 1) + c
|
||||
const m = this.P(a + 1) + b
|
||||
const n = this.P(m) + c
|
||||
const o = this.P(m + 1) + c
|
||||
|
||||
const p = this.gradDot(this.P(k), d, e, f)
|
||||
const q = this.gradDot(this.P(n), d - 1, e, f)
|
||||
const r = this.gradDot(this.P(l), d, e - 1, f)
|
||||
const s = this.gradDot(this.P(o), d - 1, e - 1, f)
|
||||
|
||||
const t = this.gradDot(this.P(k + 1), d, e, f - 1)
|
||||
const u = this.gradDot(this.P(n + 1), d - 1, e, f - 1)
|
||||
const v = this.gradDot(this.P(l + 1), d, e - 1, f - 1)
|
||||
const w = this.gradDot(this.P(o + 1), d - 1, e - 1, f - 1)
|
||||
|
||||
return lerp3(g, h, i, p, q, r, s, t, u, v, w)
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
import { PerlinNoise, Random } from 'deepslate'
|
||||
import { clampedLerp, lerp2 } from '../../Utils'
|
||||
import { PerlinNoise } from './PerlinNoise'
|
||||
|
||||
export class NoiseChunkGenerator {
|
||||
private readonly minLimitPerlinNoise: PerlinNoise
|
||||
@@ -17,11 +17,12 @@ export class NoiseChunkGenerator {
|
||||
private noiseColumnCache: (number[] | null)[] = []
|
||||
private xOffset: number = 0
|
||||
|
||||
constructor(seed: string) {
|
||||
this.minLimitPerlinNoise = PerlinNoise.fromRange(seed + 'djfqnqd', -15, 0)
|
||||
this.maxLimitPerlinNoise = PerlinNoise.fromRange(seed + 'gowdnqs', -15, 0)
|
||||
this.mainPerlinNoise = PerlinNoise.fromRange(seed + 'afiwmco', -7, 0)
|
||||
this.depthNoise = PerlinNoise.fromRange(seed + 'qphnmeo', -15, 0)
|
||||
constructor(seed: bigint) {
|
||||
const random = new Random(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])
|
||||
this.depthNoise = new PerlinNoise(random, -15, [1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1])
|
||||
}
|
||||
|
||||
public reset(settings: any, depth: number, scale: number, xOffset: number, width: number) {
|
||||
@@ -71,7 +72,7 @@ export class NoiseChunkGenerator {
|
||||
const randomDensity = this.settings.random_density_offset ? this.getRandomDensity(x) : 0
|
||||
|
||||
for (let y = 0; y <= this.chunkCountY; y += 1) {
|
||||
let noise = this.sampleAndClampNoise(x, y, this.mainPerlinNoise.getOctaveNoise(0).zo, xzScale, yScale, xzFactor, yFactor)
|
||||
let noise = this.sampleAndClampNoise(x, y, this.mainPerlinNoise.getOctaveNoise(0)!.zo, xzScale, yScale, xzFactor, yFactor)
|
||||
const yOffset = 1 - y * 2 / this.chunkCountY + randomDensity
|
||||
const density = yOffset * this.settings.density_factor + this.settings.density_offset
|
||||
const falloff = (density + this.biomeDepth) * this.biomeScale
|
||||
@@ -100,7 +101,7 @@ export class NoiseChunkGenerator {
|
||||
}
|
||||
|
||||
private getRandomDensity(x: number): number {
|
||||
const noise = this.depthNoise.getValue(x * 200, 10, this.depthNoise.getOctaveNoise(0).zo, 1, 0, true)
|
||||
const noise = this.depthNoise.sample(x * 200, 10, this.depthNoise.getOctaveNoise(0)!.zo, 1, 0, true)
|
||||
const a = (noise < 0) ? -noise * 0.3 : noise
|
||||
const b = a * 24.575625 - 2
|
||||
return (b < 0) ? b * 0.009486607142857142 : Math.min(b, 1) * 0.006640625
|
||||
@@ -120,18 +121,18 @@ export class NoiseChunkGenerator {
|
||||
|
||||
const minLimitNoise = this.minLimitPerlinNoise.getOctaveNoise(i)
|
||||
if (minLimitNoise) {
|
||||
a += minLimitNoise.noise(x2, y2, z2, e, y * e) / d
|
||||
a += minLimitNoise.sample(x2, y2, z2, e, y * e) / d
|
||||
}
|
||||
|
||||
const maxLimitNoise = this.maxLimitPerlinNoise.getOctaveNoise(i)
|
||||
if (maxLimitNoise) {
|
||||
b += maxLimitNoise.noise(x2, y2, z2, e, y * e) / d
|
||||
b += maxLimitNoise.sample(x2, y2, z2, e, y * e) / d
|
||||
}
|
||||
|
||||
if (i < 8) {
|
||||
const mainNoise = this.mainPerlinNoise.getOctaveNoise(i)
|
||||
if (mainNoise) {
|
||||
c += mainNoise.noise(
|
||||
c += mainNoise.sample(
|
||||
PerlinNoise.wrap(x * xzFactor * d),
|
||||
PerlinNoise.wrap(y * yFactor * d),
|
||||
PerlinNoise.wrap(z * xzFactor * d),
|
||||
|
||||
@@ -1,31 +0,0 @@
|
||||
import { PerlinNoise } from './PerlinNoise'
|
||||
|
||||
export class NormalNoise {
|
||||
private readonly valueFactor: number
|
||||
private readonly first: PerlinNoise
|
||||
private readonly second: PerlinNoise
|
||||
|
||||
constructor(seed: string, firstOctave: number, amplitudes: number[]) {
|
||||
this.first = new PerlinNoise(seed, firstOctave, amplitudes)
|
||||
this.second = new PerlinNoise(seed + 'a', firstOctave, amplitudes)
|
||||
|
||||
let min = +Infinity
|
||||
let max = -Infinity
|
||||
for (let i = 0; i < amplitudes.length; i += 1) {
|
||||
if (amplitudes[i] !== 0) {
|
||||
min = Math.min(min, i)
|
||||
max = Math.max(max, i)
|
||||
}
|
||||
}
|
||||
|
||||
const expectedDeviation = 0.1 * (1 + 1 / (max - min + 1))
|
||||
this.valueFactor = (1/6) / expectedDeviation
|
||||
}
|
||||
|
||||
getValue(x: number, y: number, z: number) {
|
||||
const x2 = x * 1.0181268882175227
|
||||
const y2 = y * 1.0181268882175227
|
||||
const z2 = z * 1.0181268882175227
|
||||
return (this.first.getValue(x, y, z) + this.second.getValue(x2, y2, z2)) * this.valueFactor
|
||||
}
|
||||
}
|
||||
@@ -1,54 +0,0 @@
|
||||
import seedrandom from 'seedrandom'
|
||||
import { ImprovedNoise } from './ImprovedNoise'
|
||||
|
||||
export class PerlinNoise {
|
||||
private readonly noiseLevels: ImprovedNoise[]
|
||||
private readonly amplitudes: number[]
|
||||
private readonly lowestFreqValueFactor: number
|
||||
private readonly lowestFreqInputFactor: number
|
||||
|
||||
constructor(seed: string, firstOctave: number, amplitudes: number[]) {
|
||||
this.amplitudes = amplitudes
|
||||
|
||||
this.noiseLevels = Array(this.amplitudes.length)
|
||||
for (let i = 0; i < this.amplitudes.length; i += 1) {
|
||||
this.noiseLevels[i] = new ImprovedNoise(seedrandom(seed))
|
||||
}
|
||||
|
||||
this.lowestFreqInputFactor = Math.pow(2, firstOctave)
|
||||
this.lowestFreqValueFactor = Math.pow(2, (amplitudes.length - 1)) / (Math.pow(2, amplitudes.length) - 1)
|
||||
}
|
||||
|
||||
public static fromRange(seed: string, min: number, max: number) {
|
||||
return new PerlinNoise(seed, min, Array(max - min + 1).fill(1))
|
||||
}
|
||||
|
||||
public getValue(x: number, y: number, z: number, a = 0, b = 0, fixY = false) {
|
||||
let value = 0
|
||||
let inputF = this.lowestFreqInputFactor
|
||||
let valueF = this.lowestFreqValueFactor
|
||||
for (let i = 0; i < this.noiseLevels.length; i += 1) {
|
||||
const noise = this.noiseLevels[i]
|
||||
if (noise) {
|
||||
value += this.amplitudes[i] * noise.noise(
|
||||
PerlinNoise.wrap(x * inputF),
|
||||
fixY ? -noise.yo : PerlinNoise.wrap(y * inputF),
|
||||
PerlinNoise.wrap(z * inputF),
|
||||
a * inputF,
|
||||
b * inputF
|
||||
) * valueF
|
||||
}
|
||||
inputF *= 2
|
||||
valueF /= 2
|
||||
}
|
||||
return value
|
||||
}
|
||||
|
||||
public getOctaveNoise(i: number) {
|
||||
return this.noiseLevels[this.noiseLevels.length - 1 - i]
|
||||
}
|
||||
|
||||
public static wrap(value: number) {
|
||||
return value - Math.floor(value / 3.3554432E7 + 0.5) * 3.3554432E7
|
||||
}
|
||||
}
|
||||
@@ -1,36 +0,0 @@
|
||||
import { hexId } from '../Utils'
|
||||
|
||||
export class Mounter {
|
||||
private registry: { [id: string]: (el: Element) => void } = {}
|
||||
|
||||
register(callback: (el: Element) => void): string {
|
||||
const id = hexId()
|
||||
this.registry[id] = callback
|
||||
return id
|
||||
}
|
||||
|
||||
on(type: string, callback: (el: Element) => void): string {
|
||||
return this.register(el => {
|
||||
el.addEventListener(type, evt => {
|
||||
callback(el)
|
||||
evt.stopPropagation()
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
onChange(callback: (el: Element) => void): string {
|
||||
return this.on('change', callback)
|
||||
}
|
||||
|
||||
onClick(callback: (el: Element) => void): string {
|
||||
return this.on('click', callback)
|
||||
}
|
||||
|
||||
mounted(el: Element): void {
|
||||
el.querySelectorAll('[data-id]').forEach(el => {
|
||||
const id = el.getAttribute('data-id')!
|
||||
this.registry[id]?.(el)
|
||||
})
|
||||
this.registry = {}
|
||||
}
|
||||
}
|
||||
@@ -6,13 +6,17 @@ import { Octicon } from '../components/Octicon'
|
||||
import { useFocus } from '../hooks'
|
||||
import { locale } from '../Locales'
|
||||
import type { BlockStateRegistry } from '../Schemas'
|
||||
import { hexId } from '../Utils'
|
||||
import { hexId, newSeed } from '../Utils'
|
||||
|
||||
const selectRegistries = ['loot_table.type', 'loot_entry.type', 'function.function', 'condition.condition', 'criterion.trigger', 'dimension.generator.type', 'dimension.generator.biome_source.type', 'carver.type', 'feature.type', 'decorator.type', 'feature.tree.minimum_size.type', 'block_state_provider.type', 'trunk_placer.type', 'foliage_placer.type', 'tree_decorator.type', 'int_provider.type', 'float_provider.type', 'height_provider.type', 'structure_feature.type', 'surface_builder.type', 'processor.processor_type', 'rule_test.predicate_type', 'pos_rule_test.predicate_type', 'template_element.element_type', 'block_placer.type']
|
||||
const LIST_LIMIT = 20
|
||||
const LIST_LIMIT_SHOWN = 5
|
||||
|
||||
const selectRegistries = ['loot_table.type', 'loot_entry.type', 'function.function', 'condition.condition', 'criterion.trigger', 'dimension.generator.type', 'dimension.generator.biome_source.type', 'dimension.generator.biome_source.preset', 'carver.type', 'feature.type', 'decorator.type', 'feature.tree.minimum_size.type', 'block_state_provider.type', 'trunk_placer.type', 'foliage_placer.type', 'tree_decorator.type', 'int_provider.type', 'float_provider.type', 'height_provider.type', 'structure_feature.type', 'surface_builder.type', 'processor.processor_type', 'rule_test.predicate_type', 'pos_rule_test.predicate_type', 'template_element.element_type', 'block_placer.type']
|
||||
const hiddenFields = ['number_provider.type', 'score_provider.type', 'nbt_provider.type', 'int_provider.type', 'float_provider.type', 'height_provider.type']
|
||||
const flattenedFields = ['feature.config', 'decorator.config', 'int_provider.value', 'float_provider.value', 'block_state_provider.simple_state_provider.state', 'block_state_provider.rotated_block_provider.state', 'block_state_provider.weighted_state_provider.entries.entry.data', 'rule_test.block_state', 'structure_feature.config', 'surface_builder.config', 'template_pool.elements.entry.element']
|
||||
const inlineFields = ['loot_entry.type', 'function.function', 'condition.condition', 'criterion.trigger', 'dimension.generator.type', 'dimension.generator.biome_source.type', 'feature.type', 'decorator.type', 'block_state_provider.type', 'feature.tree.minimum_size.type', 'trunk_placer.type', 'foliage_placer.type', 'tree_decorator.type', 'block_placer.type', 'rule_test.predicate_type', 'processor.processor_type', 'template_element.element_type', 'nbt_operation.op', 'number_provider.value', 'score_provider.name', 'score_provider.target', 'nbt_provider.source', 'nbt_provider.target']
|
||||
const inlineFields = ['loot_entry.type', 'function.function', 'condition.condition', 'criterion.trigger', 'dimension.generator.type', 'dimension.generator.biome_source.type', 'feature.type', 'decorator.type', 'block_state_provider.type', 'feature.tree.minimum_size.type', 'trunk_placer.type', 'foliage_placer.type', 'tree_decorator.type', 'block_placer.type', 'rule_test.predicate_type', 'processor.processor_type', 'template_element.element_type', 'nbt_operation.op', 'number_provider.value', 'score_provider.name', 'score_provider.target', 'nbt_provider.source', 'nbt_provider.target', 'generator_biome.biome']
|
||||
const nbtFields = ['function.set_nbt.tag', 'advancement.display.icon.nbt', 'text_component_object.nbt', 'entity.nbt', 'block.nbt', 'item.nbt']
|
||||
const fixedLists = ['generator_biome.parameters.temperature', 'generator_biome.parameters.humidity', 'generator_biome.parameters.continentalness', 'generator_biome.parameters.erosion', 'generator_biome.parameters.depth', 'generator_biome.parameters.weirdness', 'feature.end_spike.crystal_beam_target', 'feature.end_gateway.exit']
|
||||
|
||||
/**
|
||||
* Secondary model used to remember the keys of a map
|
||||
@@ -50,7 +54,6 @@ export const renderHtml: RenderHook = {
|
||||
const choiceContextPath = config?.choiceContext ? new Path([], [config.choiceContext]) : config?.context ? new Path([], [config.context]) : path
|
||||
const set = (value: string) => {
|
||||
const c = choices.find(c => c.type === value) ?? choice
|
||||
console.log(c)
|
||||
path.model.set(path, c.change ? c.change(value) : c.node.default())
|
||||
}
|
||||
const inject = <select value={choice.type} onChange={(e) => set((e.target as HTMLSelectElement).value)}>
|
||||
@@ -61,7 +64,21 @@ export const renderHtml: RenderHook = {
|
||||
return [prefix, <>{inject}{suffix}</>, body]
|
||||
},
|
||||
|
||||
list({ children }, path, value, lang, states) {
|
||||
list({ children, config }, path, value, lang, states) {
|
||||
const context = path.getContext().join('.')
|
||||
if (fixedLists.includes(context)) {
|
||||
const prefix = <>
|
||||
{[...Array(config.maxLength!)].map((_, i) =>
|
||||
<ErrorPopup lang={lang} path={path.modelPush(i)} />)}
|
||||
<div class="fixed-list"></div>
|
||||
</>
|
||||
const suffix = <>{[...Array(config.maxLength)].map((_, i) => {
|
||||
const child = children.hook(this, path.modelPush(i), value?.[i], lang, states)
|
||||
return child[1]
|
||||
})}</>
|
||||
return [prefix, suffix, null]
|
||||
}
|
||||
|
||||
const onAdd = () => {
|
||||
if (!Array.isArray(value)) value = []
|
||||
path.model.set(path, [children.default(), ...value])
|
||||
@@ -73,6 +90,14 @@ export const renderHtml: RenderHook = {
|
||||
const suffix = <button class="add" onClick={onAdd}>{Octicon.plus_circle}</button>
|
||||
const body = <>
|
||||
{(value && Array.isArray(value)) && value.map((cValue, index) => {
|
||||
if (value.length > LIST_LIMIT && index >= LIST_LIMIT_SHOWN && index < value.length - LIST_LIMIT_SHOWN) {
|
||||
if (index === LIST_LIMIT_SHOWN) {
|
||||
return <div class="node-entry">
|
||||
<span class="node-message">{value.length - LIST_LIMIT} hidden entries...</span>
|
||||
</div>
|
||||
}
|
||||
return null
|
||||
}
|
||||
const cPath = path.push(index).contextPush('entry')
|
||||
const onRemove = () => cPath.set(undefined)
|
||||
const onMoveUp = () => {
|
||||
@@ -215,8 +240,11 @@ function NumberSuffix({ path, config, integer, value }: NodeProps<NumberHookPara
|
||||
: integer ? parseInt(value) : parseFloat(value)
|
||||
path.model.set(path, parsed)
|
||||
}
|
||||
return <input type={config?.color ? 'color' : 'text'} onChange={onChange}
|
||||
value={config?.color ? '#' + value?.toString(16).padStart(6, '0') ?? '#000000' : value ?? ''} />
|
||||
const val = config?.color ? '#' + value?.toString(16).padStart(6, '0') ?? '#000000' : value ?? ''
|
||||
return <>
|
||||
<input type={config?.color ? 'color' : 'text'} onChange={onChange} value={val} />
|
||||
{path.equals(new Path(['generator', 'seed'])) && <button onClick={() => newSeed(path.model)}>{Octicon.sync}</button>}
|
||||
</>
|
||||
}
|
||||
|
||||
function StringSuffix({ path, getValues, config, node, value, lang, states }: NodeProps<StringHookParams>) {
|
||||
|
||||
@@ -63,6 +63,14 @@
|
||||
},
|
||||
{
|
||||
"id": "1.17",
|
||||
"refs": {
|
||||
"mcdata_master": "1.17.1",
|
||||
"vanilla_datapack_data": "1.17.1-data",
|
||||
"vanilla_datapack_summary": "1.17.1-summary"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "1.18",
|
||||
"refs": {
|
||||
"mcdata_master": "master",
|
||||
"vanilla_datapack_data": "data",
|
||||
@@ -76,7 +84,8 @@
|
||||
"id": "loot_table",
|
||||
"url": "loot-table",
|
||||
"path": "loot_tables",
|
||||
"schema": "loot_table"
|
||||
"schema": "loot_table",
|
||||
"maxVersion": "1.17"
|
||||
},
|
||||
{
|
||||
"id": "predicate",
|
||||
|
||||
@@ -38,6 +38,9 @@
|
||||
"preview": "Visualize",
|
||||
"preview.scale": "Scale",
|
||||
"preview.depth": "Depth",
|
||||
"preview.factor": "Factor",
|
||||
"preview.offset": "Offset",
|
||||
"preview.peaks": "Peaks",
|
||||
"preview.width": "Width",
|
||||
"source_placeholder": "Paste JSON content here",
|
||||
"undo": "Undo",
|
||||
|
||||
@@ -7,8 +7,8 @@
|
||||
--text-1: #ffffff;
|
||||
--text-2: #dcdcdc;
|
||||
--text-3: #c3c3c3;
|
||||
--accent-blue: #50baf9;
|
||||
--accent-red: #f1453f;
|
||||
--accent-primary: #50baf9;
|
||||
--accent-success: #3eb84f;
|
||||
--nav: #91908f;
|
||||
--nav-hover: #b4b3b0;
|
||||
--nav-faded: #4d4c4c;
|
||||
@@ -27,8 +27,8 @@
|
||||
--text-1: #000000;
|
||||
--text-2: #505050;
|
||||
--text-3: #6a6a6a;
|
||||
--accent-blue: #088cdb;
|
||||
--accent-red: #cc312c;
|
||||
--accent-primary: #088cdb;
|
||||
--accent-success: #1a7f37;
|
||||
--nav: #343a40;
|
||||
--nav-hover: #565d64;
|
||||
--nav-faded: #9fa2a7;
|
||||
@@ -48,8 +48,8 @@
|
||||
--text-1: #000000;
|
||||
--text-2: #505050;
|
||||
--text-3: #6a6a6a;
|
||||
--accent-blue: #088cdb;
|
||||
--accent-red: #cc312c;
|
||||
--accent-primary: #088cdb;
|
||||
--accent-success: #1a7f37;
|
||||
--nav: #343a40;
|
||||
--nav-hover: #565d64;
|
||||
--nav-faded: #9fa2a7;
|
||||
@@ -331,8 +331,8 @@ main.has-preview {
|
||||
}
|
||||
|
||||
.btn.active {
|
||||
color: var(--accent-blue);
|
||||
fill: var(--accent-blue);
|
||||
color: var(--accent-primary);
|
||||
fill: var(--accent-primary);
|
||||
}
|
||||
|
||||
.btn:not(.btn-input):hover {
|
||||
@@ -462,7 +462,11 @@ main.has-preview {
|
||||
}
|
||||
|
||||
.popup-action.action-preview {
|
||||
fill: var(--accent-blue);
|
||||
fill: var(--accent-primary);
|
||||
}
|
||||
|
||||
.popup-action.action-copy.active {
|
||||
fill: var(--accent-success);
|
||||
}
|
||||
|
||||
.error {
|
||||
@@ -648,7 +652,7 @@ hr {
|
||||
}
|
||||
|
||||
.ea-content strong {
|
||||
color: var(--accent-blue) !important;
|
||||
color: var(--accent-primary) !important;
|
||||
}
|
||||
|
||||
.ea-callout {
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
--node-background-label: #1b1b1b;
|
||||
--node-background-input: #272727;
|
||||
--node-text: #dadada;
|
||||
--node-text-dimmed: #b4b4b4;
|
||||
--node-selected: #ad9715;
|
||||
--node-selected-border: #8d7a0d;
|
||||
--node-add: #487c13;
|
||||
@@ -29,6 +30,7 @@
|
||||
--node-background-label: #e4e4e4;
|
||||
--node-background-input: #ffffff;
|
||||
--node-text: #000000;
|
||||
--node-text-dimmed: #2c2c2c;
|
||||
--node-selected: #f0e65e;
|
||||
--node-selected-border: #b9a327;
|
||||
--node-add: #9bd464;
|
||||
@@ -56,6 +58,7 @@
|
||||
--node-background-label: #e4e4e4;
|
||||
--node-background-input: #ffffff;
|
||||
--node-text: #000000;
|
||||
--node-text-dimmed: #2c2c2c;
|
||||
--node-selected: #f0e65e;
|
||||
--node-selected-border: #b9a327;
|
||||
--node-add: #9bd464;
|
||||
@@ -144,7 +147,8 @@
|
||||
|
||||
.node-error ~ select:last-child,
|
||||
.node-error ~ input:last-child,
|
||||
.node-error ~ input[list]:nth-last-child(2) {
|
||||
.node-error ~ input[list]:nth-last-child(2),
|
||||
.node-error + .fixed-list ~ input {
|
||||
border-color: var(--node-remove) !important;
|
||||
}
|
||||
|
||||
@@ -339,6 +343,11 @@ span.menu-item {
|
||||
color: var(--node-popup-text-dimmed);
|
||||
}
|
||||
|
||||
.node-message {
|
||||
color: var(--node-text-dimmed);
|
||||
margin: 6px 0;
|
||||
}
|
||||
|
||||
/* Node body and list entry */
|
||||
|
||||
.node {
|
||||
@@ -421,8 +430,13 @@ span.menu-item {
|
||||
width: 25px;
|
||||
}
|
||||
|
||||
.fixed-list {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.number-node input,
|
||||
.range-node input {
|
||||
.range-node input,
|
||||
.fixed-list ~ input {
|
||||
width: 100px;
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user