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

@@ -3,9 +3,9 @@ import { Router } from 'preact-router'
import '../styles/global.css'
import '../styles/nodes.css'
import { Analytics } from './Analytics.js'
import { Header } from './components/index.js'
import { Changelog, Generator, Generators, Guide, Guides, Home, Partners, Sounds, Transformation, Versions, Worldgen } from './pages/index.js'
import { cleanUrl } from './Utils.js'
import { Header } from './components/index.js'
import { Changelog, Customized, Generator, Generators, Guide, Guides, Home, Partners, Sounds, Transformation, Versions, Worldgen } from './pages/index.js'
export function App() {
const changeRoute = (e: RouterOnChangeArgs) => {
@@ -25,6 +25,7 @@ export function App() {
<Changelog path="/changelog" />
<Versions path="/versions" />
<Transformation path="/transformation" />
<Customized path="/customized" />
<Guides path="/guides" />
<Guide path="/guides/:id" />
<Generator default />

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>

View File

@@ -0,0 +1,96 @@
import { useCallback, useEffect, useErrorBoundary, useMemo, useRef, useState } from 'preact/hooks'
import config from '../Config.js'
import { writeZip } from '../Utils.js'
import { BasicSettings } from '../components/customized/BasicSettings.jsx'
import { generateCustomized } from '../components/customized/CustomizedGenerator.js'
import { CustomizedModel } from '../components/customized/CustomizedModel.js'
import { OresSettings } from '../components/customized/OresSettings.jsx'
import { StructuresSettings } from '../components/customized/StructuresSettings.jsx'
import { Btn, ErrorPanel, Footer, VersionSwitcher } from '../components/index.js'
import { useLocale, useTitle } from '../contexts/index.js'
import { useSearchParam } from '../hooks/index.js'
import { stringifySource } from '../services/Source.js'
const Tabs = ['basic', 'structures', 'ores']
interface Props {
path?: string,
}
export function Customized({}: Props) {
const { locale } = useLocale()
// const { version, changeVersion } = useVersion()
const version = '1.20'
const changeVersion = () => {}
useTitle(locale('title.customized'))
const [errorBoundary, errorRetry] = useErrorBoundary()
if (errorBoundary) {
errorBoundary.message = `Something went wrong with the customized world tool: ${errorBoundary.message}`
return <main><ErrorPanel error={errorBoundary} onDismiss={errorRetry} /></main>
}
const [tab, setTab] = useSearchParam('tab')
useEffect(() => {
if (tab === undefined || !Tabs.includes(tab)) {
setTab(Tabs[0], true)
}
}, [tab])
const [model, setModel] = useState(CustomizedModel.getDefault(version))
const changeModel = useCallback((change: Partial<CustomizedModel>) => {
setModel(m => ({ ...m, ...change }))
}, [])
const initialModel = useMemo(() => {
return CustomizedModel.getDefault(version)
}, [version])
const download = useRef<HTMLAnchorElement>(null)
const [error, setError] = useState<Error | string | null>(null)
const generate = useCallback(async () => {
if (!download.current) return
try {
const pack = await generateCustomized(model, version)
console.log('Generated customized', pack)
const entries = Object.entries(pack).flatMap(([type, files]) => {
const prefix = `data/minecraft/${type}/`
return [...files.entries()].map(([name, data]) => {
return [prefix + name + '.json', stringifySource(data, 'json')] as [string, string]
})
})
const pack_format = config.versions.find(v => v.id === version)!.pack_format
entries.push(['pack.mcmeta', stringifySource({ pack: { pack_format, description: 'Customized world from misode.github.io' } }, 'json')])
const url = await writeZip(entries)
download.current.setAttribute('href', url)
download.current.setAttribute('download', 'customized.zip')
download.current.click()
setError(null)
} catch (e) {
if (e instanceof Error) {
e.message = `Something went wrong creating the customized pack: ${e.message}`
setError(e)
}
}
}, [model, version])
return <main>
<div class="container customized">
<div class="tabs tabs-sticky">
<span class={tab === 'basic' ? 'selected' : ''} onClick={() => setTab('basic')}>{locale('customized.basic')}</span>
<span class={tab === 'structures' ? 'selected' : ''} onClick={() => setTab('structures')}>{locale('customized.structures')}</span>
<span class={tab === 'ores' ? 'selected' : ''} onClick={() => setTab('ores')}>{locale('customized.ores')}</span>
<VersionSwitcher value={version} onChange={changeVersion} allowed={['1.20']} />
</div>
<div class="customized-tab">
{tab === 'basic' && <BasicSettings {...{model, initialModel, changeModel}} />}
{tab === 'structures' && <StructuresSettings {...{model, initialModel, changeModel}} />}
{tab === 'ores' && <OresSettings {...{model, initialModel, changeModel}} />}
</div>
<div class="customized-actions">
<Btn icon="download" label="Create" class="customized-create" onClick={generate} />
<a ref={download} style="display: none;"></a>
</div>
{error && <ErrorPanel error={error} onDismiss={() => setError(null)} />}
</div>
<Footer />
</main>
}

View File

@@ -88,6 +88,9 @@ function Tools() {
const { locale } = useLocale()
return <ToolGroup title={locale('tools')}>
<ToolCard title="Customized Worlds"
link="/customized/"
desc="Create complete data packs to customize your world" />
<ToolCard title="Report Inspector" icon="report"
link="https://misode.github.io/report/"
desc="Analyse your performance reports" />

View File

@@ -1,4 +1,5 @@
export * from './Changelog.js'
export * from './Customized.jsx'
export * from './Generator.js'
export * from './Generators.jsx'
export * from './Guide.js'

View File

@@ -84,6 +84,24 @@ async function fetchBlockStateMap(version: Version, target: BlockStateRegistry)
}
}
export async function fetchBlockStates(versionId: VersionId) {
console.debug(`[fetchBlockStates] ${versionId}`)
const version = config.versions.find(v => v.id === versionId)!
const result = new Map<string, {properties: Record<string, string[]>, default: Record<string, string>}>()
try {
const data = await cachedFetch<any>(`${mcmeta(version, 'summary')}/blocks/data.min.json`)
for (const id in data) {
result.set('minecraft:' + id, {
properties: data[id][0],
default: data[id][1],
})
}
} catch (e) {
console.warn('Error occurred while fetching block states:', message(e))
}
return result
}
export async function fetchPreset(versionId: VersionId, registry: string, id: string) {
console.debug(`[fetchPreset] ${versionId} ${registry} ${id}`)
const version = config.versions.find(v => v.id === versionId)!

View File

@@ -31,6 +31,9 @@
"copy_share": "Copy share link",
"copied": "Copied!",
"copy_context": "Copy context",
"customized.basic": "Basic",
"customized.structures": "Structures",
"customized.ores": "Ores",
"cutoff": "Cutoff",
"damage_type": "Damage type",
"developed_by": "Developed by",
@@ -129,6 +132,7 @@
"theme.light": "Light",
"theme.system": "System",
"title.changelog": "Technical Changelog",
"title.customized": "Customized Worlds",
"title.generator": "%0% Generator",
"title.generator_category": "%0% Generators",
"title.generators": "Data Pack Generators",

View File

@@ -28,7 +28,10 @@
--errors-background-2: #471610;
--errors-background-3: #3f140f;
--errors-text: #ffffffcc;
--invalid-text: #fd7951;
--invalid-text: #fd7951;
--success-background-3: #143f0f;
--water-background-3: #0d3266;
--lava-background-3: #662e0d;
--text-saturation: 60%;
--text-lightness: 45%;
--editor-variable: #9CDCFE;
@@ -67,7 +70,10 @@
--errors-background-2: #c13b29;
--errors-background-3: #d8503e;
--errors-text: #000000cc;
--invalid-text: #a32600;
--invalid-text: #a32600;
--success-background-3: #6cd83e;
--water-background-3: #3e79d8;
--lava-background-3: #d86f3e;
--text-saturation: 100%;
--text-lightness: 30%;
--editor-variable: #0451A5;
@@ -108,6 +114,9 @@
--errors-background-3: #d8503e;
--errors-text: #000000cc;
--invalid-text: #a32600;
--success-background-3: #6cd83e;
--water-background-3: #3e79d8;
--lava-background-3: #d86f3e;
--text-saturation: 100%;
--text-lightness: 30%;
--editor-variable: #0451A5;
@@ -642,6 +651,256 @@ main.has-project {
margin-top: 8px;
}
.customized .ad {
margin-left: 0;
margin-right: 0;
}
.customized .version-switcher {
padding: 6px 0;
}
.customized-tab {
padding-bottom: 30px;
}
.customized-actions > span {
color: var(--text-3);
margin-left: 10px;
}
.btn.customized-create {
display: inline-flex;
background-color: var(--accent-site-1);
font-weight: bold;
padding: 20px 18px;
}
.btn.customized-create:not(.disabled):hover {
background-color: var(--accent-site-2) !important;
}
.btn.customized-create > svg {
width: 20px;
height: 20px;
margin-right: 10px;
}
.customized > .error {
margin-left: 0;
margin-right: 0;
margin-bottom: 0;
}
.customized-tab > *:not(:first-child) {
margin-top: 10px;
}
.customized-childs {
margin-top: 10px;
margin-bottom: 20px;
margin-left: 10px;
border-left: 2px solid var(--background-4);
}
.customized-childs > * {
margin-left: 10px;
}
.customized-childs > *:not(:first-child) {
margin-top: 10px;
}
.customized-input {
display: flex;
align-items: center;
}
.customized-input .customized-label {
height: 28px;
min-width: 140px;
padding-right: 10px;
display: inline-flex;
align-items: center;
}
.customized-childs .customized-label {
min-width: 118px;
color: var(--text-2);
}
.customized-input .customized-label label {
position: relative;
}
.customized-input.customized-modified > .customized-label label::after {
content: '*';
color: var(--accent-primary);
position: absolute;
left: 100%;
margin-left: 3px;
}
.customized-input .customized-label .item-display {
width: 32px;
height: 32px;
margin-right: 3px;
}
.customized-input input[type="range"] {
margin-left: 5px;
width: 200px;
-webkit-appearance: none;
appearance: none;
background-color: transparent;
border: 2px solid var(--background-4);
}
.customized-input input[type="range"]:focus {
outline: none;
border-color: var(--text-2);
}
.customized-input.customized-errored input[type="range"] {
border-color: var(--errors-background);
}
.customized-input.customized-errored input[type="range"]:focus {
border-color: var(--accent-danger);
}
/* Track */
.customized-input input[type="range"]::-webkit-slider-runnable-track {
background-color: var(--background-4);
height: 24px;
}
.customized-input input[type="range"]::-moz-range-track {
background-color: var(--background-4);
height: 24px;
}
/* Thumb */
.customized-input input[type="range"]::-webkit-slider-thumb {
-webkit-appearance: none;
appearance: none;
background-color: var(--text-2);
margin-top: -2px;
width: 10px;
height: 28px;
}
.customized-input input[type="range"]::-moz-range-thumb {
border: none;
border-radius: 0;
background-color: var(--text-2);
margin-top: -2px;
width: 10px;
height: 28px;
}
.customized-input input[type="number"],
.customized-input input[type="text"] {
margin-left: 5px;
width: 200px;
height: 28px;
padding: 3px 6px;
background-color: var(--background-1);
border: 2px solid var(--background-4);
color: var(--text-2);
font-size: 18px;
}
.customized-input input[type="number"] {
width: 100px;
}
.customized-input input[type="number"]:focus,
.customized-input input[type="text"]:focus {
outline: none;
border-color: var(--text-2);
}
.customized-input.customized-errored input[type="number"],
.customized-input.customized-errored input[type="text"] {
border-color: var(--errors-background);
}
.customized-input.customized-errored input[type="number"]:focus,
.customized-input.customized-errored input[type="text"]:focus {
border-color: var(--accent-danger);
}
.customized-input > .item-display {
margin-left: 5px;
width: 28px;
height: 28px;
}
.customized-input button {
margin-left: 5px;
height: 28px;
padding: 2px 6px;
border: 2px solid transparent;
background-color: var(--background-4);
color: var(--text-2);
fill: var(--text-2);
font-size: 18px;
}
.customized-input button:focus {
outline: none;
border-color: var(--text-2);
}
.customized-input button.customized-toggle + span {
margin-left: 3px;
}
.customized-input span + button.customized-toggle {
margin-left: 3px;
}
.customized-input button.customized-active {
background-color: var(--success-background-3);
}
.customized-input button.customized-false {
background-color: var(--errors-background-3);
}
.customized-input button.customized-true {
background-color: var(--success-background-3);
}
.customized-input button.customized-water {
background-color: var(--water-background-3);
}
.customized-input button.customized-lava {
background-color: var(--lava-background-3);
}
.customized-input button.customized-icon {
width: 28px;
padding: 0;
display: flex;
align-items: center;
justify-content: center;
}
.customized-input button.customized-error {
fill: var(--accent-danger);
}
.customized-input > .customized-input {
margin-left: 10px;
}
.customized-input > .customized-input .customized-label {
min-width: unset;;
}
.btn {
display: flex;
align-items: center;
@@ -2025,13 +2284,17 @@ hr {
fill: var(--accent-primary);
}
.version-tabs {
.version-detail .tabs {
margin-top: 20px;
}
.tabs {
display: flex;
margin: 20px 0 10px;
margin-bottom: 10px;
box-shadow: inset 0 -1px 0 var(--background-4);
}
.version-tabs > * {
.tabs > span {
border-bottom: 2px solid transparent;
padding: 8px 16px;
cursor: pointer;
@@ -2042,15 +2305,26 @@ hr {
align-items: center;
}
.version-tabs > * > svg {
.tabs > * > svg {
margin-left: 8px;
}
.version-tabs > .selected {
.tabs > .selected {
border-color: var(--text-3);
color: var(--text-1);
}
.tabs > .version-switcher {
margin-left: auto;
}
.tabs.tabs-sticky {
position: sticky;
top: 55px;
background-color: var(--background-1);
z-index: 4;
}
.ace_editor,
.ace_gutter,
.ace_gutter .ace_layer,

View File

@@ -47,7 +47,7 @@ export default defineConfig({
title: '404',
template,
}),
...['generators', 'worldgen', 'partners', 'sounds', 'changelog', 'versions', 'guides'].map(id => html({
...['generators', 'worldgen', 'partners', 'sounds', 'changelog', 'versions', 'guides', 'transformation', 'customized'].map(id => html({
fileName: `${id}/index.html`,
title: `${English[`title.${id}`] ?? ''} - ${getVersions()}`,
template,