Customized worlds (#387)

* Customized tool UI

* Working version of the customized generator

* Error reporting and only allow 1.20 for now
This commit is contained in:
Misode
2023-06-16 04:18:33 +02:00
committed by GitHub
parent 9e68e0495b
commit 64140aec92
21 changed files with 1123 additions and 14 deletions

View File

@@ -12,9 +12,10 @@ import { itemHasGlint } from './previews/LootTable.js'
interface Props {
item: ItemStack,
slotDecoration?: boolean,
tooltip?: boolean,
advancedTooltip?: boolean,
}
export function ItemDisplay({ item, slotDecoration, advancedTooltip }: Props) {
export function ItemDisplay({ item, slotDecoration, tooltip, advancedTooltip }: Props) {
const el = useRef<HTMLDivElement>(null)
const [tooltipOffset, setTooltipOffset] = useState<[number, number]>([0, 0])
const [tooltipSwap, setTooltipSwap] = useState(false)
@@ -49,13 +50,13 @@ export function ItemDisplay({ item, slotDecoration, advancedTooltip }: Props) {
</svg>}
<div class="item-slot-overlay"></div>
</>}
<div class="item-tooltip" style={tooltipOffset && {
{tooltip !== false && <div class="item-tooltip" style={tooltipOffset && {
left: (tooltipSwap ? undefined : `${tooltipOffset[0]}px`),
right: (tooltipSwap ? `${tooltipOffset[0]}px` : undefined),
top: `${tooltipOffset[1]}px`,
}}>
<ItemTooltip item={item} advanced={advancedTooltip} />
</div>
</div>}
</div>
}

View File

@@ -57,6 +57,7 @@ export const Octicon = {
terminal: <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" width="16" height="16"><path fill-rule="evenodd" d="M0 2.75C0 1.784.784 1 1.75 1h12.5c.966 0 1.75.784 1.75 1.75v10.5A1.75 1.75 0 0114.25 15H1.75A1.75 1.75 0 010 13.25V2.75zm1.75-.25a.25.25 0 00-.25.25v10.5c0 .138.112.25.25.25h12.5a.25.25 0 00.25-.25V2.75a.25.25 0 00-.25-.25H1.75zM7.25 8a.75.75 0 01-.22.53l-2.25 2.25a.75.75 0 11-1.06-1.06L5.44 8 3.72 6.28a.75.75 0 111.06-1.06l2.25 2.25c.141.14.22.331.22.53zm1.5 1.5a.75.75 0 000 1.5h3a.75.75 0 000-1.5h-3z"></path></svg>,
three_bars: <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" width="16" height="16"><path fill-rule="evenodd" d="M1 2.75A.75.75 0 011.75 2h12.5a.75.75 0 110 1.5H1.75A.75.75 0 011 2.75zm0 5A.75.75 0 011.75 7h12.5a.75.75 0 110 1.5H1.75A.75.75 0 011 7.75zM1.75 12a.75.75 0 100 1.5h12.5a.75.75 0 100-1.5H1.75z"></path></svg>,
trashcan: <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" width="16" height="16"><path fill-rule="evenodd" d="M6.5 1.75a.25.25 0 01.25-.25h2.5a.25.25 0 01.25.25V3h-3V1.75zm4.5 0V3h2.25a.75.75 0 010 1.5H2.75a.75.75 0 010-1.5H5V1.75C5 .784 5.784 0 6.75 0h2.5C10.216 0 11 .784 11 1.75zM4.496 6.675a.75.75 0 10-1.492.15l.66 6.6A1.75 1.75 0 005.405 15h5.19c.9 0 1.652-.681 1.741-1.576l.66-6.6a.75.75 0 00-1.492-.149l-.66 6.6a.25.25 0 01-.249.225h-5.19a.25.25 0 01-.249-.225l-.66-6.6z"></path></svg>,
undo: <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" width="16" height="16"><path d="M1.22 6.28a.749.749 0 0 1 0-1.06l3.5-3.5a.749.749 0 1 1 1.06 1.06L3.561 5h7.188l.001.007L10.749 5c.058 0 .116.007.171.019A4.501 4.501 0 0 1 10.5 14H8.796a.75.75 0 0 1 0-1.5H10.5a3 3 0 1 0 0-6H3.561L5.78 8.72a.749.749 0 1 1-1.06 1.06l-3.5-3.5Z"></path></svg>,
unfold: <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" width="16" height="16"><path d="M8.177.677l2.896 2.896a.25.25 0 01-.177.427H8.75v1.25a.75.75 0 01-1.5 0V4H5.104a.25.25 0 01-.177-.427L7.823.677a.25.25 0 01.354 0zM7.25 10.75a.75.75 0 011.5 0V12h2.146a.25.25 0 01.177.427l-2.896 2.896a.25.25 0 01-.354 0l-2.896-2.896A.25.25 0 015.104 12H7.25v-1.25zm-5-2a.75.75 0 000-1.5h-.5a.75.75 0 000 1.5h.5zM6 8a.75.75 0 01-.75.75h-.5a.75.75 0 010-1.5h.5A.75.75 0 016 8zm2.25.75a.75.75 0 000-1.5h-.5a.75.75 0 000 1.5h.5zM12 8a.75.75 0 01-.75.75h-.5a.75.75 0 010-1.5h.5A.75.75 0 0112 8zm2.25.75a.75.75 0 000-1.5h-.5a.75.75 0 000 1.5h.5z"></path></svg>,
unlock: <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" width="16" height="16"><path d="M5.5 4v2h7A1.5 1.5 0 0 1 14 7.5v6a1.5 1.5 0 0 1-1.5 1.5h-9A1.5 1.5 0 0 1 2 13.5v-6A1.5 1.5 0 0 1 3.499 6H4V4a4 4 0 0 1 7.371-2.154.75.75 0 0 1-1.264.808A2.5 2.5 0 0 0 5.5 4Zm-2 3.5v6h9v-6h-9Z"></path></svg>,
upload: <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" width="16" height="16"><path fill-rule="evenodd" d="M8.53 1.22a.75.75 0 00-1.06 0L3.72 4.97a.75.75 0 001.06 1.06l2.47-2.47v6.69a.75.75 0 001.5 0V3.56l2.47 2.47a.75.75 0 101.06-1.06L8.53 1.22zM3.75 13a.75.75 0 000 1.5h8.5a.75.75 0 000-1.5h-8.5z"></path></svg>,

View File

@@ -0,0 +1,60 @@
import { Identifier, ItemStack } from 'deepslate'
import { ItemDisplay } from '../ItemDisplay.jsx'
import { TextInput } from '../index.js'
import { CustomizedInput } from './CustomizedInput.jsx'
import type { CustomizedModel } from './CustomizedModel.js'
import { CustomizedSlider } from './CustomizedSlider.jsx'
import { CustomizedToggle } from './CustomizedToggle.jsx'
interface Props {
model: CustomizedModel,
initialModel: CustomizedModel,
changeModel: (model: Partial<CustomizedModel>) => void,
}
export function BasicSettings({ model, initialModel, changeModel }: Props) {
return <>
<CustomizedSlider label="Min height"
value={model.minHeight} onChange={v => changeModel({ minHeight: v })}
min={-128} max={384} step={16} initial={initialModel.minHeight}
error={model.minHeight % 16 !== 0 ? 'Min height needs to be a multiple of 16' : undefined} />
<CustomizedSlider label="Max height"
value={model.maxHeight} onChange={v => changeModel({ maxHeight: v })}
min={-128} max={384} step={16} initial={initialModel.maxHeight}
error={model.maxHeight <= model.minHeight ? 'Max height needs to be larger than Min height' : model.maxHeight % 16 !== 0 ? 'Max height needs to be a multiple of 16' : undefined} />
<CustomizedSlider label="Sea level"
value={model.seaLevel} onChange={v => changeModel({ seaLevel: v })}
min={-128} max={384} initial={initialModel.seaLevel} />
<CustomizedInput label="Oceans"
value={model.oceans} onChange={v => changeModel({ oceans: v })}
initial={initialModel.oceans}>
<button class={`customized-toggle${model.oceans === 'water' ? ' customized-water' : ''}`} onClick={() => changeModel({ oceans: 'water' })}>Water</button>
<span>/</span>
<button class={`customized-toggle${model.oceans === 'lava' ? ' customized-lava' : ''}`} onClick={() => changeModel({ oceans: 'lava' })}>Lava</button>
<span>/</span>
<button class={`customized-toggle${model.oceans != 'water' && model.oceans != 'lava' ? ' customized-active' : ''}`} onClick={() => changeModel({ oceans: 'slime_block' })}>Custom</button>
{model.oceans != 'water' && model.oceans != 'lava' && <>
<TextInput value={model.oceans} onChange={v => changeModel({ oceans: v })} />
<ItemDisplay item={new ItemStack(Identifier.parse(model.oceans), 1)} tooltip={false} />
</>}
</CustomizedInput>
<div class="customized-group">
<CustomizedToggle label="Caves"
value={model.caves} onChange={v => changeModel({ caves: v })}
initial={initialModel.caves} />
{model.caves && <div class="customized-childs">
<CustomizedToggle label="Noise caves"
value={model.noiseCaves} onChange={v => changeModel({ noiseCaves: v })}
initial={initialModel.noiseCaves} />
<CustomizedToggle label="Carver caves"
value={model.carverCaves} onChange={v => changeModel({ carverCaves: v })}
initial={initialModel.carverCaves} />
<CustomizedToggle label="Ravines"
value={model.ravines} onChange={v => changeModel({ ravines: v })}
initial={initialModel.ravines} />
</div>}
</div>
<CustomizedSlider label="Biome size"
value={model.biomeSize} onChange={v => changeModel({ biomeSize: v })}
min={1} max={8} initial={initialModel.biomeSize} />
</>
}

View File

@@ -0,0 +1,268 @@
import { Identifier } from 'deepslate'
import { deepClone, deepEqual } from '../../Utils.js'
import { fetchAllPresets, fetchBlockStates } from '../../services/DataFetcher.js'
import type { VersionId } from '../../services/Schemas.js'
import type { CustomizedOreModel } from './CustomizedModel.js'
import { CustomizedModel } from './CustomizedModel.js'
const PackTypes = ['dimension_type', 'worldgen/noise_settings', 'worldgen/noise', 'worldgen/structure_set', 'worldgen/placed_feature', 'worldgen/configured_feature'] as const
export type CustomizedPackType = typeof PackTypes[number]
export type CustomizedPack = Record<CustomizedPackType, Map<string, any>>
interface Context {
model: CustomizedModel,
initial: CustomizedModel,
version: VersionId,
blockStates: Map<string, {properties: Record<string, string[]>, default: Record<string, string>}>,
vanilla: CustomizedPack,
out: CustomizedPack,
}
export async function generateCustomized(model: CustomizedModel, version: VersionId): Promise<CustomizedPack> {
const [blockStates, ...vanillaFiles] = await Promise.all([
fetchBlockStates(version),
...PackTypes.map(t => fetchAllPresets(version, t)),
])
const ctx: Context = {
model,
initial: CustomizedModel.getDefault(version),
version,
blockStates,
vanilla: PackTypes.reduce((acc, k, i) => {
return { ...acc, [k]: vanillaFiles[i] }
}, Object.create(null)),
out: PackTypes.reduce((acc, k) => {
return { ...acc, [k]: new Map()}
}, Object.create(null)) as CustomizedPack,
}
generateDimensionType(ctx)
generateNoiseSettings(ctx)
generateClimateNoises(ctx)
generateStructures(ctx)
generateDungeonFeatures(ctx)
generateLakeFeatures(ctx)
generateOreFeatures(ctx)
return ctx.out
}
function generateDimensionType(ctx: Context) {
if (isUnchanged(ctx, 'minHeight', 'maxHeight')) return
ctx.out.dimension_type.set('overworld', {
...ctx.vanilla.dimension_type.get('overworld'),
min_y: ctx.model.minHeight,
height: ctx.model.maxHeight - ctx.model.minHeight,
logical_height: ctx.model.maxHeight - ctx.model.minHeight,
})
}
function generateNoiseSettings(ctx: Context) {
if (isUnchanged(ctx, 'seaLevel', 'oceans', 'caves', 'noiseCaves')) return
const defaultFluid = formatIdentifier(ctx.model.oceans)
const vanilla = ctx.vanilla['worldgen/noise_settings'].get('overworld')
const finalDensity = deepClone(vanilla.noise_router.final_density)
if (!ctx.model.caves || !ctx.model.noiseCaves) {
finalDensity.argument2 = 1
finalDensity.argument1.argument.argument2.argument.argument.argument2.argument2.argument2.argument2.argument2.argument2 = 'minecraft:overworld/sloped_cheese'
}
ctx.out['worldgen/noise_settings'].set('overworld', {
...vanilla,
sea_level: ctx.model.seaLevel,
default_fluid: {
Name: defaultFluid,
Properties: ctx.blockStates.get(defaultFluid)?.default,
},
noise_router: {
...vanilla.noise_router,
final_density: finalDensity,
},
})
}
function generateClimateNoises(ctx: Context) {
if (isUnchanged(ctx, 'biomeSize')) return
for (const name of ['temperature', 'vegetation', 'continentalness', 'erosion']) {
const vanilla = ctx.vanilla['worldgen/noise'].get(name)
ctx.out['worldgen/noise'].set(name, {
...vanilla,
firstOctave: vanilla.firstOctave - ctx.model.biomeSize + 4,
})
}
}
const Structures: Partial<Record<keyof CustomizedModel, string>> = {
ancientCities: 'ancient_cities',
buriedTreasure: 'buried_treasure',
desertPyramids: 'desert_pyramids',
igloos: 'igloos',
jungleTemples: 'jungle_temples',
mineshafts: 'mineshafts',
oceanMonuments: 'ocean_monuments',
oceanRuins: 'ocean_ruins',
pillagerOutposts: 'pillager_outposts',
ruinedPortals: 'ruined_portals',
shipwrecks: 'shipwrecks',
strongholds: 'strongholds',
swampHuts: 'swamp_huts',
trailRuins: 'trail_ruins',
villages: 'villages',
woodlandMansions: 'woodland_mansions',
}
function generateStructures(ctx: Context) {
for (const [key, name] of Object.entries(Structures) as [keyof CustomizedModel, string][]) {
if (isUnchanged(ctx, key) || ctx.model[key]) continue
ctx.out['worldgen/structure_set'].set(name, {
...ctx.vanilla['worldgen/structure_set'].get(name),
structures: [],
})
}
}
const DisabledFeature = {
feature: {
type: 'minecraft:no_op',
config: {},
},
placement: [],
}
function generateDungeonFeatures(ctx: Context) {
if (isUnchanged(ctx, 'dungeons', 'dungeonTries')) return
if (!ctx.model.dungeons) {
ctx.out['worldgen/placed_feature'].set('monster_room_deep', DisabledFeature)
ctx.out['worldgen/placed_feature'].set('monster_room', DisabledFeature)
} else {
const deepTries = Math.round(ctx.model.dungeonTries * 4 / 14)
const deepVanilla = ctx.vanilla['worldgen/placed_feature'].get('monster_room_deep')
ctx.out['worldgen/placed_feature'].set('monster_room_deep', {
...deepVanilla,
placement: [
{
type: 'minecraft:count',
count: deepTries,
},
...deepVanilla.placement.slice(1),
],
})
const normalVanilla = ctx.vanilla['worldgen/placed_feature'].get('monster_room')
ctx.out['worldgen/placed_feature'].set('monster_room', {
...normalVanilla,
placement: [
{
type: 'minecraft:count',
count: ctx.model.dungeonTries - deepTries,
},
...normalVanilla.placement.slice(1),
],
})
}
}
function generateLakeFeatures(ctx: Context) {
if (!isUnchanged(ctx, 'lavaLakes', 'lavaLakeRarity', 'lavaLakeRarityUnderground')) {
if (!ctx.model.lavaLakes) {
ctx.out['worldgen/placed_feature'].set('lake_lava_surface', DisabledFeature)
ctx.out['worldgen/placed_feature'].set('lake_lava_underground', DisabledFeature)
} else {
const undergroundVanilla = ctx.vanilla['worldgen/placed_feature'].get('lake_lava_underground')
ctx.out['worldgen/placed_feature'].set('lake_lava_underground', {
...undergroundVanilla,
placement: [
{
type: 'minecraft:rarity_filter',
chance: ctx.model.lavaLakeRarityUnderground,
},
...undergroundVanilla.placement.slice(1),
],
})
const surfaceVanilla = ctx.vanilla['worldgen/placed_feature'].get('lake_lava_surface')
ctx.out['worldgen/placed_feature'].set('lake_lava_surface', {
...surfaceVanilla,
placement: [
{
type: 'minecraft:rarity_filter',
chance: ctx.model.lavaLakeRarity,
},
...surfaceVanilla.placement.slice(1),
],
})
}
}
}
const Ores: Partial<Record<keyof CustomizedModel, string>> = {
dirt: 'ore_dirt',
gravel: 'ore_gravel',
graniteLower: 'ore_granite_lower',
graniteUpper: 'ore_granite_upper',
dioriteLower: 'ore_diorite_lower',
dioriteUpper: 'ore_diorite_upper',
andesiteLower: 'ore_andesite_lower',
andesiteUpper: 'ore_andesite_upper',
coalLower: 'ore_coal_lower',
coalUpper: 'ore_coal_upper',
ironSmall: 'ore_iron_small',
ironMiddle: 'ore_iron_middle',
ironUpper: 'ore_iron_upper',
copper: 'ore_copper',
copperLarge: 'ore_copper_large',
goldLower: 'ore_gold_lower',
gold: 'ore_gold',
redstoneLower: 'ore_redstone_lower',
redstone: 'ore_redstone',
lapis: 'ore_lapis',
lapisBuried: 'ore_lapis_buried',
diamond: 'ore_diamond',
diamondBuried: 'ore_diamond_buried',
diamondLarge: 'ore_diamond_large',
}
function generateOreFeatures(ctx: Context) {
for (const [key, name] of Object.entries(Ores) as [keyof CustomizedModel, string][]) {
if (isUnchanged(ctx, key)) continue
const value = ctx.model[key] as CustomizedOreModel | undefined
const initial = ctx.initial[key] as CustomizedOreModel
if (value === undefined) {
ctx.out['worldgen/placed_feature'].set(name, DisabledFeature)
} else {
const placed = deepClone(ctx.vanilla['worldgen/placed_feature'].get(name))
if (value.tries !== initial.tries) {
const modifier = placed.placement.find((m: any) => m.type === 'minecraft:count' || m.type === 'rarity_filter')
if (Number.isInteger(value.tries)) {
modifier.type = 'minecraft:count',
modifier.count = value.tries
delete modifier.chance
} else {
modifier.type = 'minecraft:rarity_filter',
modifier.chance = Math.round(1 / value.tries)
delete modifier.count
}
}
if (value.minHeight !== initial.minHeight || value.minAboveBottom !== initial.minAboveBottom || value.minBelowTop !== initial.minBelowTop || value.maxHeight !== initial.maxHeight || value.maxAboveBottom !== value.maxBelowTop || value.maxBelowTop !== initial.maxBelowTop) {
const modifier = placed.placement.find((m: any) => m.type === 'minecraft:height_range')
modifier.min_inclusive = value.minAboveBottom !== undefined ? { above_bottom: value.minAboveBottom } : value.minBelowTop !== undefined ? { below_top: value.minBelowTop } : value.minHeight !== undefined ? { absolute: value.minHeight } : modifier.min_inclusive
modifier.max_inclusive = value.maxAboveBottom !== undefined ? { above_bottom: value.maxAboveBottom } : value.maxBelowTop !== undefined ? { below_top: value.maxBelowTop } : value.maxHeight !== undefined ? { absolute: value.maxHeight } : modifier.max_inclusive
}
ctx.out['worldgen/placed_feature'].set(name, placed)
if (value.size !== initial.size) {
const reference = placed.feature.replace(/^minecraft:/, '')
const configured = ctx.vanilla['worldgen/configured_feature'].get(reference)
configured.config.size = value.size
ctx.out['worldgen/configured_feature'].set(reference, configured)
}
}
}
}
function isUnchanged(ctx: Context, ...keys: (keyof CustomizedModel)[]) {
return keys.every(k => deepEqual(ctx.model[k], ctx.initial[k]))
}
function formatIdentifier(id: string) {
try {
return Identifier.parse(id).toString()
} catch (e) {
return id
}
}

View File

@@ -0,0 +1,25 @@
import type { ComponentChildren } from 'preact'
import { deepClone, deepEqual } from '../../Utils.js'
import { Octicon } from '../index.js'
interface Props<T> {
label: ComponentChildren,
value: T,
initial?: T,
onChange: (value: T) => void,
error?: string,
children?: ComponentChildren,
trailing?: ComponentChildren,
}
export function CustomizedInput<T>({ label, value, initial, onChange, error, children, trailing }: Props<T>) {
const isModified = initial !== undefined && !deepEqual(value, initial)
return <div class={`customized-input${isModified ? ' customized-modified' : ''}${error !== undefined ? ' customized-errored' : ''}`}>
<span class="customized-label">
{typeof label === 'string' ? <label>{label}</label> : label}
</span>
{children}
{(isModified && initial != undefined) && <button class="customized-icon tooltipped tip-se" aria-label="Reset to default" onClick={() => onChange(deepClone(initial))}>{Octicon.undo}</button>}
{error !== undefined && <button class="customized-icon customized-error tooltipped tip-se" aria-label={error}>{Octicon.issue_opened}</button>}
{trailing}
</div>
}

View File

@@ -0,0 +1,139 @@
import type { VersionId } from '../../services/Schemas.js'
export interface CustomizedOreModel {
size: number,
tries: number,
minHeight?: number,
minAboveBottom?: number,
minBelowTop?: number,
maxHeight?: number,
maxBelowTop?: number,
maxAboveBottom?: number,
trapezoid?: boolean,
}
export interface CustomizedModel {
// Basic
minHeight: number,
maxHeight: number,
seaLevel: number,
oceans: string,
caves: boolean,
noiseCaves: boolean,
carverCaves: boolean,
ravines: boolean,
biomeSize: number,
// Structures
ancientCities: boolean,
buriedTreasure: boolean,
desertPyramids: boolean,
igloos: boolean,
jungleTemples: boolean,
mineshafts: boolean,
oceanMonuments: boolean,
oceanRuins: boolean,
pillagerOutposts: boolean,
ruinedPortals: boolean,
shipwrecks: boolean,
strongholds: boolean,
swampHuts: boolean,
trailRuins: boolean,
villages: boolean,
woodlandMansions: boolean,
// Features
dungeons: boolean,
dungeonTries: number,
lavaLakes: boolean,
lavaLakeRarity: number,
lavaLakeRarityUnderground: number,
// Ores
dirt: CustomizedOreModel | undefined,
gravel: CustomizedOreModel | undefined,
graniteLower: CustomizedOreModel | undefined,
graniteUpper: CustomizedOreModel | undefined,
dioriteLower: CustomizedOreModel | undefined,
dioriteUpper: CustomizedOreModel | undefined,
andesiteLower: CustomizedOreModel | undefined,
andesiteUpper: CustomizedOreModel | undefined,
coalLower: CustomizedOreModel | undefined,
coalUpper: CustomizedOreModel | undefined,
ironSmall: CustomizedOreModel | undefined,
ironMiddle: CustomizedOreModel | undefined,
ironUpper: CustomizedOreModel | undefined,
copper: CustomizedOreModel | undefined,
copperLarge: CustomizedOreModel | undefined,
goldLower: CustomizedOreModel | undefined,
gold: CustomizedOreModel | undefined,
redstoneLower: CustomizedOreModel | undefined,
redstone: CustomizedOreModel | undefined,
lapis: CustomizedOreModel | undefined,
lapisBuried: CustomizedOreModel | undefined,
diamond: CustomizedOreModel | undefined,
diamondBuried: CustomizedOreModel | undefined,
diamondLarge: CustomizedOreModel | undefined,
}
export namespace CustomizedModel {
export function getDefault(_version: VersionId): CustomizedModel {
const model: CustomizedModel = {
minHeight: -64,
maxHeight: 320,
seaLevel: 63,
oceans: 'water',
caves: true,
noiseCaves: true,
carverCaves: true,
ravines: true,
biomeSize: 4,
ancientCities: true,
buriedTreasure: true,
desertPyramids: true,
igloos: true,
jungleTemples: true,
mineshafts: true,
oceanMonuments: true,
oceanRuins: true,
pillagerOutposts: true,
ruinedPortals: true,
shipwrecks: true,
strongholds: true,
swampHuts: true,
trailRuins: true,
villages: true,
woodlandMansions: true,
dungeons: true,
dungeonTries: 14,
lavaLakes: true,
lavaLakeRarity: 200,
lavaLakeRarityUnderground: 9,
dirt: { size: 33, tries: 7, minHeight: 0, maxHeight: 160 },
gravel: { size: 33, tries: 14, minAboveBottom: 0, maxBelowTop: 0 },
graniteLower: { size: 64, tries: 2, minHeight: 0, maxHeight: 60 },
graniteUpper: { size: 64, tries: 1/6, minHeight: 64, maxHeight: 128 },
dioriteLower: { size: 64, tries: 2, minHeight: 0, maxHeight: 60 },
dioriteUpper: { size: 64, tries: 1/6, minHeight: 64, maxHeight: 128 },
andesiteLower: { size: 64, tries: 2, minHeight: 0, maxHeight: 60 },
andesiteUpper: { size: 64, tries: 1/6, minHeight: 64, maxHeight: 128 },
coalLower: { size: 17, tries: 20, minHeight: 0, maxHeight: 192, trapezoid: true },
coalUpper: { size: 17, tries: 30, minHeight: 136, maxBelowTop: 0 },
ironSmall: { size: 4, tries: 10, minAboveBottom: 0, maxHeight: 72 },
ironMiddle: { size: 9, tries: 10, minHeight: -24, maxHeight: 56, trapezoid: true },
ironUpper: { size: 9, tries: 90, minHeight: 80, maxHeight: 384, trapezoid: true },
copper: { size: 10, tries: 16, minHeight: -16, maxHeight: 112, trapezoid: true },
copperLarge: { size: 20, tries: 16, minHeight: -16, maxHeight: 112, trapezoid: true },
goldLower: { size: 9, tries: 1/2, minHeight: -64, maxBelowTop: -48 },
gold: { size: 9, tries: 4, minHeight: -64, maxBelowTop: 32, trapezoid: true },
redstoneLower: { size: 8, tries: 8, minAboveBottom: -32, maxAboveBottom: 32, trapezoid: true },
redstone: { size: 8, tries: 4, minAboveBottom: 0, maxHeight: 15 },
lapis: { size: 7, tries: 2, minAboveBottom: -32, maxAboveBottom: 32, trapezoid: true },
lapisBuried: { size: 7, tries: 4, minAboveBottom: 0, maxHeight: 32 },
diamond: { size: 4, tries: 7, minAboveBottom: -80, maxAboveBottom: 80, trapezoid: true },
diamondBuried: { size: 8, tries: 4, minAboveBottom: -80, maxAboveBottom: 80, trapezoid: true },
diamondLarge: { size: 12, tries: 1/9, minAboveBottom: -80, maxAboveBottom: 80, trapezoid: true },
}
return model
}
}

View File

@@ -0,0 +1,42 @@
import { useCallback } from 'preact/hooks'
import type { CustomizedModel, CustomizedOreModel } from './CustomizedModel.js'
import { CustomizedSlider } from './CustomizedSlider.jsx'
interface Props {
model: CustomizedModel,
value: CustomizedOreModel,
initial: CustomizedOreModel,
onChange: (value: CustomizedOreModel) => void,
}
export function CustomizedOre({ model, value, initial, onChange }: Props) {
const changeOre = useCallback((change: Partial<CustomizedOreModel>) => {
onChange({ ...value, ...change })
}, [value])
return <>
<CustomizedSlider label="Size" value={value.size} onChange={v => changeOre({ size: v })} min={1} max={64} initial={initial.size} />
{Number.isInteger(value.tries)
? <CustomizedSlider label="Tries"
value={value.tries} onChange={v => changeOre({ tries: v })}
min={1} max={100} initial={initial.tries}/>
: <CustomizedSlider label="Rarity"
value={Math.round(1 / value.tries)} onChange={v => changeOre({ tries: 1 / v })}
min={1} max={100} initial={Math.round(1 / initial.tries)} />}
<CustomizedSlider label={value.trapezoid ? 'Min triangle' : 'Min height'}
value={calcHeight(model, value.minAboveBottom, value.minBelowTop, value.minHeight) ?? 0}
onChange={v => changeOre(value.minAboveBottom !== undefined ? { minAboveBottom: v - model.minHeight } : value.minBelowTop != undefined ? { minBelowTop: model.maxHeight - v } : { minHeight: v })}
min={-64} max={320} initial={calcHeight(model, initial.minAboveBottom, initial.minBelowTop, initial.minHeight) ?? 0} />
<CustomizedSlider label={value.trapezoid ? 'Max triangle' : 'Max height'}
value={calcHeight(model, value.maxAboveBottom, value.maxBelowTop, value.maxHeight) ?? 0}
onChange={v => changeOre(value.maxAboveBottom !== undefined ? { maxAboveBottom: v - model.minHeight } : value.maxBelowTop != undefined ? { maxBelowTop: model.maxHeight - v } : { maxHeight: v })}
min={-64} max={320} initial={calcHeight(model, initial.maxAboveBottom, initial.maxBelowTop, initial.maxHeight) ?? 0} />
</>
}
function calcHeight(model: CustomizedModel, aboveBottom: number | undefined, belowTop: number | undefined, absolute: number | undefined) {
return aboveBottom !== undefined
? (model.minHeight + aboveBottom)
: belowTop !== undefined
? (model.maxHeight - belowTop)
: absolute
}

View File

@@ -0,0 +1,32 @@
import { Identifier, ItemStack } from 'deepslate'
import { deepClone } from '../../Utils.js'
import { ItemDisplay } from '../ItemDisplay.jsx'
import { CustomizedInput } from './CustomizedInput.jsx'
import type { CustomizedModel, CustomizedOreModel } from './CustomizedModel.js'
import { CustomizedOre } from './CustomizedOre.jsx'
interface Props {
label: string,
item?: string,
ores: (keyof CustomizedModel)[],
model: CustomizedModel,
initialModel: CustomizedModel,
changeModel: (model: Partial<CustomizedModel>) => void,
}
export function CustomizedOreGroup({ label, item, ores, model, initialModel, changeModel }: Props) {
const isEnabled = ores.every(k => model[k] != undefined)
return <div class="customized-group">
<CustomizedInput label={<>
{item != undefined && <ItemDisplay item={new ItemStack(Identifier.parse(item), 1)} tooltip={false} />}
<label>{label}</label>
</>} value={ores.map(k => model[k])} initial={ores.map(k => initialModel[k])} onChange={() => changeModel(ores.reduce((acc, k) => ({ ...acc, [k]: deepClone(initialModel[k])}), {}))}>
<button class={`customized-toggle${!isEnabled ? ' customized-false' : ''}`} onClick={() => changeModel(ores.reduce((acc, k) => ({ ...acc, [k]: undefined}), {}))}>No</button>
<span>/</span>
<button class={`customized-toggle${isEnabled ? ' customized-true' : ''}`} onClick={() => isEnabled || changeModel(ores.reduce((acc, k) => ({ ...acc, [k]: deepClone(initialModel[k])}), {}))}>Yes</button>
</CustomizedInput>
{isEnabled && ores.map(k => <div key={k} class="customized-childs">
<CustomizedOre model={model} value={model[k] as CustomizedOreModel} onChange={v => changeModel({ [k]: v })} initial={initialModel[k] as CustomizedOreModel} />
</div>)}
</div>
}

View File

@@ -0,0 +1,22 @@
import type { NodeChildren } from '@mcschema/core'
import { NumberInput, RangeInput } from '../index.js'
import { CustomizedInput } from './CustomizedInput.jsx'
interface Props {
label: string,
value: number,
min: number,
max: number,
step?: number,
initial?: number,
error?: string,
onChange: (value: number) => void,
children?: NodeChildren,
}
export function CustomizedSlider(props: Props) {
const isInteger = (props.step ?? 1) >= 1
return <CustomizedInput {...props}>
<RangeInput value={props.value} min={props.min} max={props.max} step={props.step ?? 1} onChange={props.onChange} />
<NumberInput value={isInteger ? props.value : props.value.toFixed(3)} step={Math.max(1, props.step ?? 1)} onChange={props.onChange} />
</CustomizedInput>
}

View File

@@ -0,0 +1,17 @@
import type { ComponentChildren } from 'preact'
import { CustomizedInput } from './CustomizedInput.jsx'
interface Props {
label: string,
value: boolean,
initial?: boolean,
onChange: (value: boolean) => void,
children?: ComponentChildren,
}
export function CustomizedToggle(props: Props) {
return <CustomizedInput {...props} trailing={props.children}>
<button class={`customized-toggle${!props.value ? ' customized-false' : ''}`} onClick={() => props.onChange(false)}>No</button>
<span>/</span>
<button class={`customized-toggle${props.value ? ' customized-true' : ''}`} onClick={() => props.onChange(true)}>Yes</button>
</CustomizedInput>
}

View File

@@ -0,0 +1,24 @@
import type { CustomizedModel } from './CustomizedModel.js'
import { CustomizedOreGroup } from './CustomizedOreGroup.jsx'
interface Props {
model: CustomizedModel,
initialModel: CustomizedModel,
changeModel: (model: Partial<CustomizedModel>) => void,
}
export function OresSettings(props: Props) {
return <>
<CustomizedOreGroup label="Dirt" item="dirt" ores={['dirt']} {...props} />
<CustomizedOreGroup label="Gravel" item="gravel" ores={['gravel']} {...props} />
<CustomizedOreGroup label="Granite" item="granite" ores={['graniteLower', 'graniteUpper']} {...props} />
<CustomizedOreGroup label="Diorite" item="diorite" ores={['dioriteLower', 'dioriteUpper']} {...props} />
<CustomizedOreGroup label="Andesite" item="andesite" ores={['andesiteLower', 'andesiteUpper']} {...props} />
<CustomizedOreGroup label="Coal" item="coal_ore" ores={['coalLower', 'coalUpper']} {...props} />
<CustomizedOreGroup label="Iron" item="iron_ore" ores={['ironSmall', 'ironMiddle', 'ironUpper']} {...props} />
<CustomizedOreGroup label="Copper" item="copper_ore" ores={['copper', 'copperLarge']} {...props} />
<CustomizedOreGroup label="Gold" item="gold_ore" ores={['goldLower', 'gold']} {...props} />
<CustomizedOreGroup label="Redstone" item="redstone_ore" ores={['redstoneLower', 'redstone']} {...props} />
<CustomizedOreGroup label="Lapis Lazuli" item="lapis_ore" ores={['lapis', 'lapisBuried']} {...props} />
<CustomizedOreGroup label="Diamond" item="diamond_ore" ores={['diamond', 'diamondBuried', 'diamondLarge']} {...props} />
</>
}

View File

@@ -0,0 +1,81 @@
import type { CustomizedModel } from './CustomizedModel.js'
import { CustomizedSlider } from './CustomizedSlider.jsx'
import { CustomizedToggle } from './CustomizedToggle.jsx'
interface Props {
model: CustomizedModel,
initialModel: CustomizedModel,
changeModel: (model: Partial<CustomizedModel>) => void,
}
export function StructuresSettings({ model, initialModel, changeModel }: Props) {
return <>
<CustomizedToggle label="Ancient cities"
value={model.ancientCities} onChange={v => changeModel({ ancientCities: v })}
initial={initialModel.ancientCities} />
<CustomizedToggle label="Buried treasure"
value={model.buriedTreasure} onChange={v => changeModel({ buriedTreasure: v })}
initial={initialModel.buriedTreasure} />
<CustomizedToggle label="Desert pyramids"
value={model.desertPyramids} onChange={v => changeModel({ desertPyramids: v })}
initial={initialModel.desertPyramids} />
<CustomizedToggle label="Igloos"
value={model.igloos} onChange={v => changeModel({ igloos: v })}
initial={initialModel.igloos} />
<CustomizedToggle label="Jungle temples"
value={model.jungleTemples} onChange={v => changeModel({ jungleTemples: v })}
initial={initialModel.jungleTemples} />
<CustomizedToggle label="Mineshafts"
value={model.mineshafts} onChange={v => changeModel({ mineshafts: v })}
initial={initialModel.mineshafts} />
<CustomizedToggle label="Ocean monuments"
value={model.oceanMonuments} onChange={v => changeModel({ oceanMonuments: v })}
initial={initialModel.oceanMonuments} />
<CustomizedToggle label="Ocean ruins"
value={model.oceanRuins} onChange={v => changeModel({ oceanRuins: v })}
initial={initialModel.oceanRuins} />
<CustomizedToggle label="Pillager outposts"
value={model.pillagerOutposts} onChange={v => changeModel({ pillagerOutposts: v })}
initial={initialModel.pillagerOutposts} />
<CustomizedToggle label="Ruined portals"
value={model.ruinedPortals} onChange={v => changeModel({ ruinedPortals: v })}
initial={initialModel.ruinedPortals} />
<CustomizedToggle label="Shipwrecks"
value={model.shipwrecks} onChange={v => changeModel({ shipwrecks: v })}
initial={initialModel.shipwrecks} />
<CustomizedToggle label="Strongholds"
value={model.strongholds} onChange={v => changeModel({ strongholds: v })}
initial={initialModel.strongholds} />
<CustomizedToggle label="Swamp huts"
value={model.swampHuts} onChange={v => changeModel({ swampHuts: v })}
initial={initialModel.swampHuts} />
<CustomizedToggle label="Trail ruins"
value={model.trailRuins} onChange={v => changeModel({ trailRuins: v })}
initial={initialModel.trailRuins} />
<CustomizedToggle label="Villages"
value={model.villages} onChange={v => changeModel({ villages: v })}
initial={initialModel.villages} />
<CustomizedToggle label="Woodland mansions"
value={model.woodlandMansions} onChange={v => changeModel({ woodlandMansions: v })}
initial={initialModel.woodlandMansions} />
<CustomizedToggle label="Dungeons"
value={model.dungeons} onChange={v => changeModel({ dungeons: v })}
initial={initialModel.dungeons}>
{model.dungeons && <CustomizedSlider label="Tries"
value={model.dungeonTries} onChange={v => changeModel({ dungeonTries: v })}
min={1} max={256} initial={initialModel.dungeonTries} />}
</CustomizedToggle>
<div class="customized-group">
<CustomizedToggle label="Lava lakes"
value={model.lavaLakes} onChange={v => changeModel({ lavaLakes: v })}
initial={initialModel.lavaLakes} />
{model.lavaLakes && <div class="customized-childs">
<CustomizedSlider label="Surface rarity"
value={model.lavaLakeRarity} onChange={v => changeModel({ lavaLakeRarity: v })}
min={1} max={400} initial={initialModel.lavaLakeRarity} />
<CustomizedSlider label="Underground rarity"
value={model.lavaLakeRarityUnderground} onChange={v => changeModel({ lavaLakeRarityUnderground: v })}
min={1} max={400} initial={initialModel.lavaLakeRarityUnderground} />
</div>}
</div>
</>
}

View File

@@ -50,7 +50,7 @@ export function VersionDetail({ id, version }: Props) {
This version does not exist. Only versions since 1.14 are tracked, or it may be too recent.
</p>}
</div>
<div class="version-tabs">
<div class="tabs">
<span class={tab === 'changelog' ? 'selected' : ''} onClick={() => setTab('changelog')}>{locale('versions.technical_changes')}</span>
<span class={tab === 'discussion' ? 'selected' : ''} onClick={() => setTab('discussion')}>{locale('versions.discussion')}</span>
<span class={tab === 'fixes' ? 'selected' : ''} onClick={() => setTab('fixes')}>{locale('versions.fixes')}</span>