From 64140aec92c02ae153df0edca6ed15e41a006e71 Mon Sep 17 00:00:00 2001
From: Misode
Date: Fri, 16 Jun 2023 04:18:33 +0200
Subject: [PATCH] Customized worlds (#387)
* Customized tool UI
* Working version of the customized generator
* Error reporting and only allow 1.20 for now
---
src/app/App.tsx | 5 +-
src/app/components/ItemDisplay.tsx | 7 +-
src/app/components/Octicon.tsx | 1 +
.../components/customized/BasicSettings.tsx | 60 ++++
.../customized/CustomizedGenerator.ts | 268 ++++++++++++++++
.../components/customized/CustomizedInput.tsx | 25 ++
.../components/customized/CustomizedModel.ts | 139 +++++++++
.../components/customized/CustomizedOre.tsx | 42 +++
.../customized/CustomizedOreGroup.tsx | 32 ++
.../customized/CustomizedSlider.tsx | 22 ++
.../customized/CustomizedToggle.tsx | 17 ++
.../components/customized/OresSettings.tsx | 24 ++
.../customized/StructuresSettings.tsx | 81 +++++
src/app/components/versions/VersionDetail.tsx | 2 +-
src/app/pages/Customized.tsx | 96 ++++++
src/app/pages/Home.tsx | 3 +
src/app/pages/index.ts | 1 +
src/app/services/DataFetcher.ts | 18 ++
src/locales/en.json | 4 +
src/styles/global.css | 288 +++++++++++++++++-
vite.config.js | 2 +-
21 files changed, 1123 insertions(+), 14 deletions(-)
create mode 100644 src/app/components/customized/BasicSettings.tsx
create mode 100644 src/app/components/customized/CustomizedGenerator.ts
create mode 100644 src/app/components/customized/CustomizedInput.tsx
create mode 100644 src/app/components/customized/CustomizedModel.ts
create mode 100644 src/app/components/customized/CustomizedOre.tsx
create mode 100644 src/app/components/customized/CustomizedOreGroup.tsx
create mode 100644 src/app/components/customized/CustomizedSlider.tsx
create mode 100644 src/app/components/customized/CustomizedToggle.tsx
create mode 100644 src/app/components/customized/OresSettings.tsx
create mode 100644 src/app/components/customized/StructuresSettings.tsx
create mode 100644 src/app/pages/Customized.tsx
diff --git a/src/app/App.tsx b/src/app/App.tsx
index 0a359ff6..b9ccce5f 100644
--- a/src/app/App.tsx
+++ b/src/app/App.tsx
@@ -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() {
+
diff --git a/src/app/components/ItemDisplay.tsx b/src/app/components/ItemDisplay.tsx
index 12c81424..b0e5e88b 100644
--- a/src/app/components/ItemDisplay.tsx
+++ b/src/app/components/ItemDisplay.tsx
@@ -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(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) {
}
>}
-
-
+ }
}
diff --git a/src/app/components/Octicon.tsx b/src/app/components/Octicon.tsx
index 1e0ef8bf..69af9f77 100644
--- a/src/app/components/Octicon.tsx
+++ b/src/app/components/Octicon.tsx
@@ -57,6 +57,7 @@ export const Octicon = {
terminal: ,
three_bars: ,
trashcan: ,
+ undo: ,
unfold: ,
unlock: ,
upload: ,
diff --git a/src/app/components/customized/BasicSettings.tsx b/src/app/components/customized/BasicSettings.tsx
new file mode 100644
index 00000000..919e0807
--- /dev/null
+++ b/src/app/components/customized/BasicSettings.tsx
@@ -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) => void,
+}
+export function BasicSettings({ model, initialModel, changeModel }: Props) {
+ return <>
+ 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} />
+ 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} />
+ changeModel({ seaLevel: v })}
+ min={-128} max={384} initial={initialModel.seaLevel} />
+ changeModel({ oceans: v })}
+ initial={initialModel.oceans}>
+ changeModel({ oceans: 'water' })}>Water
+ /
+ changeModel({ oceans: 'lava' })}>Lava
+ /
+ changeModel({ oceans: 'slime_block' })}>Custom
+ {model.oceans != 'water' && model.oceans != 'lava' && <>
+ changeModel({ oceans: v })} />
+
+ >}
+
+
+
changeModel({ caves: v })}
+ initial={initialModel.caves} />
+ {model.caves &&
+ changeModel({ noiseCaves: v })}
+ initial={initialModel.noiseCaves} />
+ changeModel({ carverCaves: v })}
+ initial={initialModel.carverCaves} />
+ changeModel({ ravines: v })}
+ initial={initialModel.ravines} />
+
}
+
+ changeModel({ biomeSize: v })}
+ min={1} max={8} initial={initialModel.biomeSize} />
+ >
+}
diff --git a/src/app/components/customized/CustomizedGenerator.ts b/src/app/components/customized/CustomizedGenerator.ts
new file mode 100644
index 00000000..20272480
--- /dev/null
+++ b/src/app/components/customized/CustomizedGenerator.ts
@@ -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>
+
+interface Context {
+ model: CustomizedModel,
+ initial: CustomizedModel,
+ version: VersionId,
+ blockStates: Map, default: Record}>,
+ vanilla: CustomizedPack,
+ out: CustomizedPack,
+}
+
+export async function generateCustomized(model: CustomizedModel, version: VersionId): Promise {
+ 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> = {
+ 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> = {
+ 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
+ }
+}
diff --git a/src/app/components/customized/CustomizedInput.tsx b/src/app/components/customized/CustomizedInput.tsx
new file mode 100644
index 00000000..2c1d9370
--- /dev/null
+++ b/src/app/components/customized/CustomizedInput.tsx
@@ -0,0 +1,25 @@
+import type { ComponentChildren } from 'preact'
+import { deepClone, deepEqual } from '../../Utils.js'
+import { Octicon } from '../index.js'
+
+interface Props {
+ label: ComponentChildren,
+ value: T,
+ initial?: T,
+ onChange: (value: T) => void,
+ error?: string,
+ children?: ComponentChildren,
+ trailing?: ComponentChildren,
+}
+export function CustomizedInput({ label, value, initial, onChange, error, children, trailing }: Props) {
+ const isModified = initial !== undefined && !deepEqual(value, initial)
+ return
+
+ {typeof label === 'string' ? {label} : label}
+
+ {children}
+ {(isModified && initial != undefined) && onChange(deepClone(initial))}>{Octicon.undo} }
+ {error !== undefined && {Octicon.issue_opened} }
+ {trailing}
+
+}
diff --git a/src/app/components/customized/CustomizedModel.ts b/src/app/components/customized/CustomizedModel.ts
new file mode 100644
index 00000000..c725b740
--- /dev/null
+++ b/src/app/components/customized/CustomizedModel.ts
@@ -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
+ }
+}
diff --git a/src/app/components/customized/CustomizedOre.tsx b/src/app/components/customized/CustomizedOre.tsx
new file mode 100644
index 00000000..efef5ba6
--- /dev/null
+++ b/src/app/components/customized/CustomizedOre.tsx
@@ -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) => {
+ onChange({ ...value, ...change })
+ }, [value])
+
+ return <>
+ changeOre({ size: v })} min={1} max={64} initial={initial.size} />
+ {Number.isInteger(value.tries)
+ ? changeOre({ tries: v })}
+ min={1} max={100} initial={initial.tries}/>
+ : changeOre({ tries: 1 / v })}
+ min={1} max={100} initial={Math.round(1 / initial.tries)} />}
+ 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} />
+ 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
+}
diff --git a/src/app/components/customized/CustomizedOreGroup.tsx b/src/app/components/customized/CustomizedOreGroup.tsx
new file mode 100644
index 00000000..7c92aa3b
--- /dev/null
+++ b/src/app/components/customized/CustomizedOreGroup.tsx
@@ -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) => void,
+}
+export function CustomizedOreGroup({ label, item, ores, model, initialModel, changeModel }: Props) {
+ const isEnabled = ores.every(k => model[k] != undefined)
+
+ return
+
+ {item != undefined && }
+ {label}
+ >} value={ores.map(k => model[k])} initial={ores.map(k => initialModel[k])} onChange={() => changeModel(ores.reduce((acc, k) => ({ ...acc, [k]: deepClone(initialModel[k])}), {}))}>
+ changeModel(ores.reduce((acc, k) => ({ ...acc, [k]: undefined}), {}))}>No
+ /
+ isEnabled || changeModel(ores.reduce((acc, k) => ({ ...acc, [k]: deepClone(initialModel[k])}), {}))}>Yes
+
+ {isEnabled && ores.map(k =>
+ changeModel({ [k]: v })} initial={initialModel[k] as CustomizedOreModel} />
+
)}
+
+}
diff --git a/src/app/components/customized/CustomizedSlider.tsx b/src/app/components/customized/CustomizedSlider.tsx
new file mode 100644
index 00000000..f39a4c6f
--- /dev/null
+++ b/src/app/components/customized/CustomizedSlider.tsx
@@ -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
+
+
+
+}
diff --git a/src/app/components/customized/CustomizedToggle.tsx b/src/app/components/customized/CustomizedToggle.tsx
new file mode 100644
index 00000000..60c74d23
--- /dev/null
+++ b/src/app/components/customized/CustomizedToggle.tsx
@@ -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
+ props.onChange(false)}>No
+ /
+ props.onChange(true)}>Yes
+
+}
diff --git a/src/app/components/customized/OresSettings.tsx b/src/app/components/customized/OresSettings.tsx
new file mode 100644
index 00000000..8cecbf46
--- /dev/null
+++ b/src/app/components/customized/OresSettings.tsx
@@ -0,0 +1,24 @@
+import type { CustomizedModel } from './CustomizedModel.js'
+import { CustomizedOreGroup } from './CustomizedOreGroup.jsx'
+
+interface Props {
+ model: CustomizedModel,
+ initialModel: CustomizedModel,
+ changeModel: (model: Partial) => void,
+}
+export function OresSettings(props: Props) {
+ return <>
+
+
+
+
+
+
+
+
+
+
+
+
+ >
+}
diff --git a/src/app/components/customized/StructuresSettings.tsx b/src/app/components/customized/StructuresSettings.tsx
new file mode 100644
index 00000000..b0b0cfd2
--- /dev/null
+++ b/src/app/components/customized/StructuresSettings.tsx
@@ -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) => void,
+}
+export function StructuresSettings({ model, initialModel, changeModel }: Props) {
+ return <>
+ changeModel({ ancientCities: v })}
+ initial={initialModel.ancientCities} />
+ changeModel({ buriedTreasure: v })}
+ initial={initialModel.buriedTreasure} />
+ changeModel({ desertPyramids: v })}
+ initial={initialModel.desertPyramids} />
+ changeModel({ igloos: v })}
+ initial={initialModel.igloos} />
+ changeModel({ jungleTemples: v })}
+ initial={initialModel.jungleTemples} />
+ changeModel({ mineshafts: v })}
+ initial={initialModel.mineshafts} />
+ changeModel({ oceanMonuments: v })}
+ initial={initialModel.oceanMonuments} />
+ changeModel({ oceanRuins: v })}
+ initial={initialModel.oceanRuins} />
+ changeModel({ pillagerOutposts: v })}
+ initial={initialModel.pillagerOutposts} />
+ changeModel({ ruinedPortals: v })}
+ initial={initialModel.ruinedPortals} />
+ changeModel({ shipwrecks: v })}
+ initial={initialModel.shipwrecks} />
+ changeModel({ strongholds: v })}
+ initial={initialModel.strongholds} />
+ changeModel({ swampHuts: v })}
+ initial={initialModel.swampHuts} />
+ changeModel({ trailRuins: v })}
+ initial={initialModel.trailRuins} />
+ changeModel({ villages: v })}
+ initial={initialModel.villages} />
+ changeModel({ woodlandMansions: v })}
+ initial={initialModel.woodlandMansions} />
+ changeModel({ dungeons: v })}
+ initial={initialModel.dungeons}>
+ {model.dungeons && changeModel({ dungeonTries: v })}
+ min={1} max={256} initial={initialModel.dungeonTries} />}
+
+
+
changeModel({ lavaLakes: v })}
+ initial={initialModel.lavaLakes} />
+ {model.lavaLakes &&
+ changeModel({ lavaLakeRarity: v })}
+ min={1} max={400} initial={initialModel.lavaLakeRarity} />
+ changeModel({ lavaLakeRarityUnderground: v })}
+ min={1} max={400} initial={initialModel.lavaLakeRarityUnderground} />
+
}
+
+ >
+}
diff --git a/src/app/components/versions/VersionDetail.tsx b/src/app/components/versions/VersionDetail.tsx
index 662a18c9..3681e548 100644
--- a/src/app/components/versions/VersionDetail.tsx
+++ b/src/app/components/versions/VersionDetail.tsx
@@ -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.
}
-
+
setTab('changelog')}>{locale('versions.technical_changes')}
setTab('discussion')}>{locale('versions.discussion')}
setTab('fixes')}>{locale('versions.fixes')}
diff --git a/src/app/pages/Customized.tsx b/src/app/pages/Customized.tsx
new file mode 100644
index 00000000..e97533f1
--- /dev/null
+++ b/src/app/pages/Customized.tsx
@@ -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
+ }
+
+ 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
) => {
+ setModel(m => ({ ...m, ...change }))
+ }, [])
+ const initialModel = useMemo(() => {
+ return CustomizedModel.getDefault(version)
+ }, [version])
+
+ const download = useRef(null)
+ const [error, setError] = useState(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
+
+
+ setTab('basic')}>{locale('customized.basic')}
+ setTab('structures')}>{locale('customized.structures')}
+ setTab('ores')}>{locale('customized.ores')}
+
+
+
+ {tab === 'basic' && }
+ {tab === 'structures' && }
+ {tab === 'ores' && }
+
+
+ {error &&
setError(null)} />}
+
+
+
+}
diff --git a/src/app/pages/Home.tsx b/src/app/pages/Home.tsx
index b8c59254..525e7e0e 100644
--- a/src/app/pages/Home.tsx
+++ b/src/app/pages/Home.tsx
@@ -88,6 +88,9 @@ function Tools() {
const { locale } = useLocale()
return
+
diff --git a/src/app/pages/index.ts b/src/app/pages/index.ts
index 9017ab18..fe69d102 100644
--- a/src/app/pages/index.ts
+++ b/src/app/pages/index.ts
@@ -1,4 +1,5 @@
export * from './Changelog.js'
+export * from './Customized.jsx'
export * from './Generator.js'
export * from './Generators.jsx'
export * from './Guide.js'
diff --git a/src/app/services/DataFetcher.ts b/src/app/services/DataFetcher.ts
index 258e7b88..18c1031e 100644
--- a/src/app/services/DataFetcher.ts
+++ b/src/app/services/DataFetcher.ts
@@ -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, default: Record}>()
+ try {
+ const data = await cachedFetch(`${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)!
diff --git a/src/locales/en.json b/src/locales/en.json
index 44fe933b..2622b384 100644
--- a/src/locales/en.json
+++ b/src/locales/en.json
@@ -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",
diff --git a/src/styles/global.css b/src/styles/global.css
index a6b6fcfb..f66df0ee 100644
--- a/src/styles/global.css
+++ b/src/styles/global.css
@@ -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,
diff --git a/vite.config.js b/vite.config.js
index 62903550..d5c98815 100644
--- a/vite.config.js
+++ b/vite.config.js
@@ -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,