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}> + + / + + / + + {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} + + {children} + {(isModified && initial != undefined) && } + {error !== undefined && } + {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 && } + + } value={ores.map(k => model[k])} initial={ores.map(k => initialModel[k])} onChange={() => changeModel(ores.reduce((acc, k) => ({ ...acc, [k]: deepClone(initialModel[k])}), {}))}> + + / + + + {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 + + / + + +} 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,