mirror of
https://github.com/misode/misode.github.io.git
synced 2026-04-23 07:10:41 +00:00
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:
@@ -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 />
|
||||
|
||||
@@ -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>
|
||||
}
|
||||
|
||||
|
||||
@@ -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>,
|
||||
|
||||
60
src/app/components/customized/BasicSettings.tsx
Normal file
60
src/app/components/customized/BasicSettings.tsx
Normal 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} />
|
||||
</>
|
||||
}
|
||||
268
src/app/components/customized/CustomizedGenerator.ts
Normal file
268
src/app/components/customized/CustomizedGenerator.ts
Normal 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
|
||||
}
|
||||
}
|
||||
25
src/app/components/customized/CustomizedInput.tsx
Normal file
25
src/app/components/customized/CustomizedInput.tsx
Normal 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>
|
||||
}
|
||||
139
src/app/components/customized/CustomizedModel.ts
Normal file
139
src/app/components/customized/CustomizedModel.ts
Normal 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
|
||||
}
|
||||
}
|
||||
42
src/app/components/customized/CustomizedOre.tsx
Normal file
42
src/app/components/customized/CustomizedOre.tsx
Normal 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
|
||||
}
|
||||
32
src/app/components/customized/CustomizedOreGroup.tsx
Normal file
32
src/app/components/customized/CustomizedOreGroup.tsx
Normal 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>
|
||||
}
|
||||
22
src/app/components/customized/CustomizedSlider.tsx
Normal file
22
src/app/components/customized/CustomizedSlider.tsx
Normal 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>
|
||||
}
|
||||
17
src/app/components/customized/CustomizedToggle.tsx
Normal file
17
src/app/components/customized/CustomizedToggle.tsx
Normal 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>
|
||||
}
|
||||
24
src/app/components/customized/OresSettings.tsx
Normal file
24
src/app/components/customized/OresSettings.tsx
Normal 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} />
|
||||
</>
|
||||
}
|
||||
81
src/app/components/customized/StructuresSettings.tsx
Normal file
81
src/app/components/customized/StructuresSettings.tsx
Normal 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>
|
||||
</>
|
||||
}
|
||||
@@ -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>
|
||||
|
||||
96
src/app/pages/Customized.tsx
Normal file
96
src/app/pages/Customized.tsx
Normal 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>
|
||||
}
|
||||
@@ -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" />
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
export * from './Changelog.js'
|
||||
export * from './Customized.jsx'
|
||||
export * from './Generator.js'
|
||||
export * from './Generators.jsx'
|
||||
export * from './Guide.js'
|
||||
|
||||
@@ -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)!
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user