From ac259bb83e008f723d4b3831fc0fdf736b103529 Mon Sep 17 00:00:00 2001 From: Misode Date: Thu, 13 Oct 2022 02:05:33 +0200 Subject: [PATCH] Add loot table preview (#293) * Initial loot table preview + item display counts * Add loot functions without NBT * Add loot conditions * Render item tooltips with name and lore components * Remove debug text component * Disable advanced tooltips in the tree * Minor style fixes * Add item slot overlay and tweak tooltip offset * Fix some items not rendering * Translate item names and text components * Translate params + more functions and tooltips * Add durability bar * Configurable stack mixing * Correct tooltip background and border * Add enchanting * Enchantment glint * Configurable luck, daytime and weather * Improve tooltip spacing * More tooltip spacing improvements * Remove debug logging --- package-lock.json | 14 +- package.json | 2 +- {src => public}/.nojekyll | 0 public/fonts/seven.ttf | Bin 0 -> 188416 bytes public/images/container.png | Bin 0 -> 417 bytes public/images/glint.png | Bin 0 -> 24860 bytes public/images/tooltip.png | Bin 0 -> 158 bytes {src => public}/sitemap.txt | 0 src/app/Utils.ts | 19 + src/app/components/ItemDisplay.tsx | 89 +- src/app/components/ItemTooltip.tsx | 57 + src/app/components/TextComponent.tsx | 129 +++ src/app/components/generator/PreviewPanel.tsx | 8 +- .../previews/BiomeSourcePreview.tsx | 2 - .../components/previews/LootTablePreview.tsx | 79 ++ src/app/previews/BiomeSource.ts | 2 - src/app/previews/LootTable.ts | 994 ++++++++++++++++++ src/app/schema/renderHtml.tsx | 2 +- src/app/services/DataFetcher.ts | 10 + src/app/services/Resources.ts | 82 +- src/locales/en.json | 6 + src/styles/global.css | 184 +++- src/styles/nodes.css | 5 + vite.config.js | 2 - 24 files changed, 1630 insertions(+), 56 deletions(-) rename {src => public}/.nojekyll (100%) create mode 100644 public/fonts/seven.ttf create mode 100644 public/images/container.png create mode 100644 public/images/glint.png create mode 100644 public/images/tooltip.png rename {src => public}/sitemap.txt (100%) create mode 100644 src/app/components/ItemTooltip.tsx create mode 100644 src/app/components/TextComponent.tsx create mode 100644 src/app/components/previews/LootTablePreview.tsx create mode 100644 src/app/previews/LootTable.ts diff --git a/package-lock.json b/package-lock.json index 13fe256e..08c1df2e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -22,7 +22,7 @@ "brace": "^0.11.1", "buffer": "^6.0.3", "comment-json": "^4.1.1", - "deepslate": "^0.13.0", + "deepslate": "^0.13.2", "deepslate-1.18": "npm:deepslate@^0.9.0-beta.9", "deepslate-1.18.2": "npm:deepslate@^0.9.0-beta.13", "highlight.js": "^11.5.1", @@ -1933,9 +1933,9 @@ "dev": true }, "node_modules/deepslate": { - "version": "0.13.0", - "resolved": "https://registry.npmjs.org/deepslate/-/deepslate-0.13.0.tgz", - "integrity": "sha512-16Dh/dOc8RLtiL0aQ3/h7bHUcIer+jAfFXehavhvHquEkxncQyZDq9YGktPTm9gTD2VGTDZeuIwZWAiMYkdfqw==", + "version": "0.13.2", + "resolved": "https://registry.npmjs.org/deepslate/-/deepslate-0.13.2.tgz", + "integrity": "sha512-6pa9mgPu4A+RqYoN7AH79oKzzSNfvCJsrBKHE+AQjt20Uo33qJIRNG+2+sFHx84PAPJ3Z1CCnWWV+kBniD8E2g==", "dependencies": { "gl-matrix": "^3.3.0", "md5": "^2.3.0", @@ -6621,9 +6621,9 @@ "dev": true }, "deepslate": { - "version": "0.13.0", - "resolved": "https://registry.npmjs.org/deepslate/-/deepslate-0.13.0.tgz", - "integrity": "sha512-16Dh/dOc8RLtiL0aQ3/h7bHUcIer+jAfFXehavhvHquEkxncQyZDq9YGktPTm9gTD2VGTDZeuIwZWAiMYkdfqw==", + "version": "0.13.2", + "resolved": "https://registry.npmjs.org/deepslate/-/deepslate-0.13.2.tgz", + "integrity": "sha512-6pa9mgPu4A+RqYoN7AH79oKzzSNfvCJsrBKHE+AQjt20Uo33qJIRNG+2+sFHx84PAPJ3Z1CCnWWV+kBniD8E2g==", "requires": { "gl-matrix": "^3.3.0", "md5": "^2.3.0", diff --git a/package.json b/package.json index 449672a3..41011aa2 100644 --- a/package.json +++ b/package.json @@ -28,7 +28,7 @@ "brace": "^0.11.1", "buffer": "^6.0.3", "comment-json": "^4.1.1", - "deepslate": "^0.13.0", + "deepslate": "^0.13.2", "deepslate-1.18": "npm:deepslate@^0.9.0-beta.9", "deepslate-1.18.2": "npm:deepslate@^0.9.0-beta.13", "highlight.js": "^11.5.1", diff --git a/src/.nojekyll b/public/.nojekyll similarity index 100% rename from src/.nojekyll rename to public/.nojekyll diff --git a/public/fonts/seven.ttf b/public/fonts/seven.ttf new file mode 100644 index 0000000000000000000000000000000000000000..0b738f7f84a4a9ce6ff32f483f21b3bc2712e4a3 GIT binary patch literal 188416 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

y@+4mi-v6iZ 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 MP4HpeeDss~KSfLoRsaA1 literal 0 HcmV?d00001 diff --git a/public/images/container.png b/public/images/container.png new file mode 100644 index 0000000000000000000000000000000000000000..8a33932a0f2aa539799223d28a06709fb791795c GIT binary patch literal 417 zcmeAS@N?(olHy`uVBq!ia0vp^8-O^FgBeJ=7T)m&QjEnx?oJHr&dIz4a#+$GeH|GX zHuiJ>Nn{1`MFV_7T!Hle|NoC2I~E!mYHn`c-Q5ioN!>hM8c4C11o;I6Wr2V}%A45@ zD8yOd5n0T@z;_sg8IR|$NC8^H*Z%xhzrIJ$_vYjd=h%v3 z&l#6Dd`w`8e!rIQ?~1p%E1Qo={}x(vyvF@QRBNvFyl$txe8&n6dYdz4d>$uAv0t{B zv8?EW_U7YOKR2H|Se;Zkt8fEDZ!>F#jE=+O1V$-#?h6(X4aW{LnOtrDe%}12{JgZF zf<4#$5A1ecGdWm|{rHpp4eixup=$WV7NBUK_3Gdq--mjWn;!42W4b4sGx{v92&2jy#(ieYu9ey6T VxBmHT6<`oCc)I$ztaD0e0suwdrPTlc literal 0 HcmV?d00001 diff --git a/public/images/glint.png b/public/images/glint.png new file mode 100644 index 0000000000000000000000000000000000000000..0a495e1d8976764b47e00f63d43dc3565ef1637d GIT binary patch literal 24860 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%0AB=inZWo1{vFT^c|Rl;-ckhg9LDS% zo2th?EV;ngVfqmN%Idq)vf5+1``CdaRMkA2n+#)9B*QwCAafUUxF(Kl0bTn)2RSBT z;;HzAvg7W~x}0elbp%sR=9(Zl7n#`7nV0)JOO(Bm=&Rtb$53MA58JahDJCGW1AQb^ zByZE7h0T9e#V2L`98+gf(-Fym@YcHUwo{ift6;WIl;ve^-R)IRyY zvSQK>gTG%Qv{CY%cGwDgp$@B@_=I}77HH#nFj3d`!M{N7l?za{x3~& z*CPkGipXG~?tP%3=T&?*h$x++mx-&oTEF+>>-&Y&ULy{2lvg47t9x7*8CL~VZ`~XN zm)LGlA-1N$#~YQHLu%;oo8EQv)7iiAvq~{mon5j>p70}crNeL9cqX*3S zd%CMWOJM|WAG0P_ycq3vu51~s2`s|&i*+z|;AWR^XPKB^uB52hT@G9jH}RF}W`7p| zkCEh?yKr2Sf-75(%+yw14Fe{NMuVYsHG4bsMBTEg5_2{|0uIS~;64hA=kS<1w^ew1 z7-xLpY{hbB5&khps}?kIsIC$LU!s-L#R`uK2JJkt5as=JB90gHt~UJpaOG@E?&?fe*Qoc+PQcuPS3b146^&A$L@yzATQH#i3ZHGlRVo7goNbHTQ#7VfbCwOllVf zRAmq1XQWN#7J(aYL$!5v$0K`E_g#%W+?R>+<-u*IOA!0ppMK8nL7c%Yk$6wS%!g9+ zMQty@oyr&8kFPdDO)RA1fE%-&kPsdLzX0_-fnrJ+5l-bcSM1tID7jJ2l@1b@2tRj- z+Wb>Iist<8#nQSVU;Ntbfj6fKhaZ}5htv}_8d;k8d4Ie)5hnzL`vE z4SY{Pb(XJ#>xwvE^J(U!*cxa0ix8q2CwNX`#X-K96*x*sGaLQ+{4>K2dHHCSL{)P= zuB%qqbgh+naKMszl2%!4*u7|8bw-3HJ_4(QfHU}*Rb3$&P*WAoUDISkTJRc(l>!mY zy5?kka|lEynY*^f@(zS4tPR(s>nkj-uo8?gOOHsbfHH+@{dV4j21?c+hQ4+K+TQye zp0;Iktb_j0+|FUwL$XJSS#we>l*&R4A$)ce!j-k@k2Oi;H(siJh|SxS})kcSN>?i{zI+wIXnF2T*0@O|JC*$JNCdZ9`!_22Oi*d;1L7 z$Fo`S9WnC-31Y>F;B{EoA^&Mm4S2-5ZL!UPb2e|n?d{ZFtxj)VmzT^ZZ0kgv*>iW$yl6m`&tKool`v`x`GU>;xnCcKN{W*6c5!lf>d%ActjR6Fz_7&Va6R3v)=**jXhl)LnOkJQy7@y)c%PpNU_-c|E+P5LukT} x_spV62^9x-v^H{{Vk=Hy(Cb(u)jd;Ql|eI;pKpb)Vhm6_gQu&X%Q~loCIDZyDhmJr literal 0 HcmV?d00001 diff --git a/src/sitemap.txt b/public/sitemap.txt similarity index 100% rename from src/sitemap.txt rename to public/sitemap.txt diff --git a/src/app/Utils.ts b/src/app/Utils.ts index 864312a4..f65b0ad4 100644 --- a/src/app/Utils.ts +++ b/src/app/Utils.ts @@ -1,6 +1,7 @@ import type { DataModel } from '@mcschema/core' import { Path } from '@mcschema/core' import * as zip from '@zip.js/zip.js' +import type { Random } from 'deepslate/core' import yaml from 'js-yaml' import { route } from 'preact-router' import rfdc from 'rfdc' @@ -337,3 +338,21 @@ export async function computeIfAbsentAsync(map: Map, key: K, getter: map.set(key, value) return value } + +export function getWeightedRandom(random: Random, entries: T[], getWeight: (entry: T) => number) { + let totalWeight = 0 + for (const entry of entries) { + totalWeight += getWeight(entry) + } + if (totalWeight <= 0) { + return undefined + } + let n = random.nextInt(totalWeight) + for (const entry of entries) { + n -= getWeight(entry) + if (n < 0) { + return entry + } + } + return undefined +} diff --git a/src/app/components/ItemDisplay.tsx b/src/app/components/ItemDisplay.tsx index 60fc82ea..6cb89f2e 100644 --- a/src/app/components/ItemDisplay.tsx +++ b/src/app/components/ItemDisplay.tsx @@ -1,55 +1,100 @@ -import { useState } from 'preact/hooks' +import { useEffect, useRef, useState } from 'preact/hooks' import { useVersion } from '../contexts/Version.jsx' import { useAsync } from '../hooks/useAsync.js' +import type { Item } from '../previews/LootTable.js' +import { MaxDamageItems } from '../previews/LootTable.js' import { getAssetUrl } from '../services/DataFetcher.js' import { renderItem } from '../services/Resources.js' import { getCollections } from '../services/Schemas.js' +import { ItemTooltip } from './ItemTooltip.jsx' import { Octicon } from './Octicon.jsx' interface Props { - item: string, + item: Item, + slotDecoration?: boolean, + advancedTooltip?: boolean, } -export function ItemDisplay({ item }: Props) { +export function ItemDisplay({ item, slotDecoration, advancedTooltip }: Props) { + const el = useRef(null) + const [tooltipOffset, setTooltipOffset] = useState<[number, number]>([0, 0]) + const [tooltipSwap, setTooltipSwap] = useState(false) + + useEffect(() => { + const onMove = (e: MouseEvent) => { + requestAnimationFrame(() => { + const { right, width } = el.current!.getBoundingClientRect() + const swap = right + 200 > document.body.clientWidth + setTooltipSwap(swap) + setTooltipOffset([(swap ? width - e.offsetX : e.offsetX) + 20, e.offsetY - 40]) + }) + } + el.current?.addEventListener('mousemove', onMove) + return () => el.current?.removeEventListener('mousemove', onMove) + }, []) + + const maxDamage = MaxDamageItems.get(item.id) + + return

+} + +function ItemItself({ item }: Props) { const { version } = useVersion() const [errored, setErrored] = useState(false) - if (errored || (item.includes(':') && !item.startsWith('minecraft:'))) { - return
- {Octicon.package} -
+ const isEnchanted = (item.tag?.Enchantments?.length ?? 0) > 0 || (item.tag?.StoredEnchantments?.length ?? 0) > 0 + + if (errored || (item.id.includes(':') && !item.id.startsWith('minecraft:'))) { + return Octicon.package } const { value: collections } = useAsync(() => getCollections(version), []) if (collections === undefined) { - return
+ return null } - const texturePath = `item/${item.replace(/^minecraft:/, '')}` + const texturePath = `item/${item.id.replace(/^minecraft:/, '')}` if (collections.get('texture').includes('minecraft:' + texturePath)) { - return
- setErrored(true)} /> -
+ const src = getAssetUrl(version, 'textures', texturePath) + return <> + setErrored(true)} draggable={false} /> + {isEnchanted &&
} + } - const modelPath = `block/${item.replace(/^minecraft:/, '')}` + const modelPath = `item/${item.id.replace(/^minecraft:/, '')}` if (collections.get('model').includes('minecraft:' + modelPath)) { - return
- -
+ return } - return
- {Octicon.package} -
+ return Octicon.package } -function RenderedItem({ item }: Props) { +function RenderedItem({ item, isEnchanted }: Props & { isEnchanted: boolean }) { const { version } = useVersion() - const { value: src } = useAsync(() => renderItem(version, item), [version, item]) + const { value: src } = useAsync(() => renderItem(version, item.id), [version, item]) if (src) { - return {item} + return <> + {item.id} + {isEnchanted &&
} + } return
diff --git a/src/app/components/ItemTooltip.tsx b/src/app/components/ItemTooltip.tsx new file mode 100644 index 00000000..7fb2edb8 --- /dev/null +++ b/src/app/components/ItemTooltip.tsx @@ -0,0 +1,57 @@ +import { useVersion } from '../contexts/Version.jsx' +import { useAsync } from '../hooks/useAsync.js' +import { getEnchantmentData, MaxDamageItems } from '../previews/LootTable.js' +import { getTranslation } from '../services/Resources.js' +import { TextComponent } from './TextComponent.jsx' + +interface Props { + id: string, + tag?: any, + advanced?: boolean, + offset?: [number, number], + swap?: boolean, +} +export function ItemTooltip({ id, tag, advanced, offset = [0, 0], swap }: Props) { + const { version } = useVersion() + const { value: translatedName } = useAsync(() => { + const key = id.split(':').join('.') + return getTranslation(version, `item.${key}`) ?? getTranslation(version, `block.${key}`) + }, [version, id]) + const displayName = tag?.display?.Name + const name = displayName ? JSON.parse(displayName) : (translatedName ?? fakeTranslation(id)) + + const maxDamage = MaxDamageItems.get(id) + + return
+ + {tag?.Enchantments?.map(({ id, lvl }: { id: string, lvl: number }) => { + const ench = getEnchantmentData(id) + const component: any[] = [{ translate: `enchantment.${id.replace(':', '.')}`, color: ench?.curse ? 'red' : 'gray' }] + if (lvl !== 1 || ench?.maxLevel !== 1) { + component.push(' ', { translate: `enchantment.level.${lvl}`}) + } + return + })} + {tag?.display && <> + {tag?.display?.color && (advanced + ? + : )} + {(tag?.display?.Lore ?? []).map((line: any) => )} + } + {tag?.Unbreakable === true && } + {(advanced && (tag?.Damage ?? 0) > 0 && maxDamage) && } + {advanced && <> + + {tag && } + } +
+} + +function fakeTranslation(str: string) { + const raw = str.replace(/minecraft:/, '').replaceAll('_', ' ') + return raw[0].toUpperCase() + raw.slice(1) +} diff --git a/src/app/components/TextComponent.tsx b/src/app/components/TextComponent.tsx new file mode 100644 index 00000000..2e0b26c9 --- /dev/null +++ b/src/app/components/TextComponent.tsx @@ -0,0 +1,129 @@ +import { useMemo } from 'preact/hooks' +import { useVersion } from '../contexts/Version.jsx' +import { useAsync } from '../hooks/useAsync.js' +import { getTranslation } from '../services/Resources.js' + +interface StyleData { + color?: string, + bold?: boolean, + italic?: boolean, + underlined?: boolean, + strikethrough?: boolean, +} + +interface PartData extends StyleData { + text?: string, + translate?: string, + with?: string[], +} + +interface Props { + component: unknown, + base?: StyleData, + shadow?: boolean, +} +export function TextComponent({ component, base = { color: 'white' }, shadow = true }: Props) { + const state = JSON.stringify(component) + const parts = useMemo(() => { + const parts: PartData[] = [] + visitComponent(component, el => parts.push(el)) + return parts + }, [state]) + + return
+ {shadow &&
+ {parts.map(p => )} +
} +
+ {parts.map(p => )} +
+
+} + +function visitComponent(component: unknown, consumer: (c: PartData) => void) { + if (typeof component === 'string' || typeof component === 'number') { + consumer({ text: component.toString() }) + } else if (Array.isArray(component)) { + const base = component[0] + visitComponent(base, consumer) + for (const c of component.slice(1)) { + visitComponent(c, d => consumer(inherit(d, base))) + } + } else if (typeof component === 'object' && component !== null) { + if ('text' in component) { + consumer(component) + } else if ('translate' in component) { + consumer(component) + } else if ('score' in component) { + consumer({ ...component, text: '123' }) + } else if ('selector' in component) { + consumer({ ...component, text: 'Steve' }) + } else if ('keybind' in component) { + consumer({ ...component, text: (component as any).keybind }) + } else if ('nbt' in component) { + consumer({ ...component, text: (component as any).nbt }) + } + if ('extra' in component) { + for (const e of (component as any).extra) { + visitComponent(e, c => consumer(inherit(c, component))) + } + } + } +} + +function inherit(component: object, base: PartData) { + return { + color: base.color, + bold: base.bold, + italic: base.italic, + underlined: base.underlined, + strikethrough: base.strikethrough, + ...component, + } +} + +const TextColors = { + black: ['#000', '#000'], + dark_blue: ['#00A', '#00002A'], + dark_green: ['#0A0', '#002A00'], + dark_aqua: ['#0AA', '#002A2A'], + dark_red: ['#A00', '#2A0000'], + dark_purple: ['#A0A', '#2A002A'], + gold: ['#FA0', '#2A2A00'], + gray: ['#AAA', '#2A2A2A'], + dark_gray: ['#555', '#151515'], + blue: ['#55F', '#15153F'], + green: ['#5F5', '#153F15'], + aqua: ['#5FF', '#153F3F'], + red: ['#F55', '#3F1515'], + light_purple: ['#F5F', '#3F153F'], + yellow: ['#FF5', '#3F3F15'], + white: ['#FFF', '#3F3F3F'], +} + +type TextColorKey = keyof typeof TextColors +const TextColorKeys = Object.keys(TextColors) + +function TextPart({ part, shadow }: { part: PartData, shadow?: boolean }) { + if (part.translate) { + const { version } = useVersion() + const { value: translated } = useAsync(() => { + return getTranslation(version, part.translate!, part.with) + }, [version, part.translate, ...part.with ?? []]) + return {translated ?? part.translate} + } + return {part.text} +} + +function createStyle(style: StyleData, shadow?: boolean) { + return { + color: style.color && (TextColorKeys.includes(style.color) + ? TextColors[style.color as TextColorKey][shadow ? 1 : 0] + : shadow ? 'transparent' : style.color), + fontWeight: (style.bold === true) ? 'bold' : undefined, + fontStyle: (style.italic === true) ? 'italic' : undefined, + textDecoration: (style.underlined === true) + ? (style.strikethrough === true) ? 'underline line-through' : 'underline' + : (style.strikethrough === true) ? 'line-through' : undefined, + } +} diff --git a/src/app/components/generator/PreviewPanel.tsx b/src/app/components/generator/PreviewPanel.tsx index 46049809..35a4f04b 100644 --- a/src/app/components/generator/PreviewPanel.tsx +++ b/src/app/components/generator/PreviewPanel.tsx @@ -5,8 +5,9 @@ import { useModel } from '../../hooks/index.js' import type { VersionId } from '../../services/index.js' import { checkVersion } from '../../services/index.js' import { BiomeSourcePreview, DecoratorPreview, DensityFunctionPreview, NoisePreview, NoiseSettingsPreview } from '../previews/index.js' +import { LootTablePreview } from '../previews/LootTablePreview.jsx' -export const HasPreview = ['dimension', 'worldgen/density_function', 'worldgen/noise', 'worldgen/noise_settings', 'worldgen/configured_feature', 'worldgen/placed_feature'] +export const HasPreview = ['loot_table', 'dimension', 'worldgen/density_function', 'worldgen/noise', 'worldgen/noise_settings', 'worldgen/configured_feature', 'worldgen/placed_feature'] type PreviewPanelProps = { model: DataModel | undefined, @@ -24,6 +25,11 @@ export function PreviewPanel({ model, version, id, shown }: PreviewPanelProps) { if (!model) return <> + if (id === 'loot_table') { + const data = model.get(new Path([])) + if (data) return + } + if (id === 'dimension' && model.get(new Path(['generator', 'type']))?.endsWith('noise')) { const data = model.get(new Path(['generator', 'biome_source'])) if (data) return diff --git a/src/app/components/previews/BiomeSourcePreview.tsx b/src/app/components/previews/BiomeSourcePreview.tsx index 5dbfb5a2..ac9aa69b 100644 --- a/src/app/components/previews/BiomeSourcePreview.tsx +++ b/src/app/components/previews/BiomeSourcePreview.tsx @@ -63,8 +63,6 @@ export const BiomeSourcePreview = ({ model, data, shown, version }: PreviewProps } }, [version, state, scale, seed, yOffset, shown, biomeColors, project]) - console.log(yOffset) - const changeScale = (newScale: number) => { newScale = Math.max(1, Math.round(newScale)) offset.current[0] = offset.current[0] * scale / newScale diff --git a/src/app/components/previews/LootTablePreview.tsx b/src/app/components/previews/LootTablePreview.tsx new file mode 100644 index 00000000..cdef22cd --- /dev/null +++ b/src/app/components/previews/LootTablePreview.tsx @@ -0,0 +1,79 @@ +import { DataModel } from '@mcschema/core' +import { useEffect, useRef, useState } from 'preact/hooks' +import { useLocale, useVersion } from '../../contexts/index.js' +import type { SlottedItem } from '../../previews/LootTable.js' +import { generateLootTable } from '../../previews/LootTable.js' +import { clamp, randomSeed } from '../../Utils.js' +import { Btn, BtnMenu, NumberInput } from '../index.js' +import { ItemDisplay } from '../ItemDisplay.jsx' +import type { PreviewProps } from './index.js' + +export const LootTablePreview = ({ data }: PreviewProps) => { + const { locale } = useLocale() + const { version } = useVersion() + const [seed, setSeed] = useState(randomSeed()) + const [luck, setLuck] = useState(0) + const [daytime, setDaytime] = useState(0) + const [weather, setWeather] = useState('clear') + const [mixItems, setMixItems] = useState(true) + const [advancedTooltips, setAdvancedTooltips] = useState(true) + const overlay = useRef(null) + + const [items, setItems] = useState([]) + + const table = DataModel.unwrapLists(data) + const state = JSON.stringify(table) + useEffect(() => { + const items = generateLootTable(table, { version, seed, luck, daytime, weather, stackMixer: mixItems ? 'container' : 'default' }) + setItems(items) + }, [version, seed, luck, daytime, weather, mixItems, state]) + + return <> +
+ Container background + {items.map(({ slot, item }) => +
+ +
+ )} +
+
+ +
e.stopPropagation()}> + {locale('preview.luck')} + +
+
e.stopPropagation()}> + {locale('preview.daytime')} + +
+
e.stopPropagation()}> + {locale('preview.weather')} + +
+ {setMixItems(!mixItems); e.stopPropagation()}} /> + {setAdvancedTooltips(!advancedTooltips); e.stopPropagation()}} /> +
+ setSeed(randomSeed())} /> +
+ +} + +const GUI_WIDTH = 176 +const GUI_HEIGHT = 81 +const SLOT_SIZE = 18 + +function slotStyle(slot: number) { + slot = clamp(slot, 0, 26) + const x = (slot % 9) * SLOT_SIZE + 7 + const y = (Math.floor(slot / 9)) * SLOT_SIZE + 20 + return { + left: `${x*100/GUI_WIDTH}%`, + top: `${y*100/GUI_HEIGHT}%`, + width: `${SLOT_SIZE*100/GUI_WIDTH}%`, + height: `${SLOT_SIZE*100/GUI_HEIGHT}%`, + } +} diff --git a/src/app/previews/BiomeSource.ts b/src/app/previews/BiomeSource.ts index eebbf9ab..3e401649 100644 --- a/src/app/previews/BiomeSource.ts +++ b/src/app/previews/BiomeSource.ts @@ -67,8 +67,6 @@ export async function getBiome(state: any, x: number, z: number, options: BiomeS const xx = Math.floor(centerX + ((x - 100) * quartStep)) const zz = Math.floor(centerZ + ((z - 100) * quartStep)) - console.log('get biome', options.y) - const { palette, data } = DEEPSLATE.fillBiomes(xx * 4, xx * 4 + 4, zz * 4, zz * 4 + 4, 1, options.y) const biome = palette.get(data[0])! diff --git a/src/app/previews/LootTable.ts b/src/app/previews/LootTable.ts new file mode 100644 index 00000000..0137e2bb --- /dev/null +++ b/src/app/previews/LootTable.ts @@ -0,0 +1,994 @@ +import type { Random } from 'deepslate' +import { LegacyRandom } from 'deepslate' +import type { VersionId } from '../services/Schemas.js' +import { clamp, deepClone, getWeightedRandom, isObject } from '../Utils.js' + +export interface Item { + id: string, + count: number, + tag?: any, +} + +export interface SlottedItem { + slot: number, + item: Item, +} + +type ItemConsumer = (item: Item) => void + +const StackMixers = { + container: fillContainer, + default: assignSlots, +} + +type StackMixer = keyof typeof StackMixers + +interface LootOptions { + version: VersionId, + seed: bigint, + luck: number, + daytime: number, + weather: string, + stackMixer: StackMixer, +} + +interface LootContext extends LootOptions { + random: Random, + luck: number + weather: string, + dayTime: number, + getItemTag(id: string): string[], + getLootTable(id: string): any, + getPredicate(id: string): any, +} + +export function generateLootTable(lootTable: any, options: LootOptions) { + const ctx = createLootContext(options) + const result: Item[] = [] + generateTable(lootTable, item => result.push(item), ctx) + const mixer = StackMixers[options.stackMixer] + return mixer(result, ctx) +} + +const SLOT_COUNT = 27 + +function fillContainer(items: Item[], ctx: LootContext): SlottedItem[] { + const slots = shuffle([...Array(SLOT_COUNT)].map((_, i) => i), ctx) + + const queue = items.filter(i => i.id !== 'minecraft:air' && i.count > 1) + items = items.filter(i => i.id !== 'minecraft:air' && i.count === 1) + + while (SLOT_COUNT - items.length - queue.length > 0 && queue.length > 0) { + const [itemA] = queue.splice(ctx.random.nextInt(queue.length), 1) + const splitCount = ctx.random.nextInt(Math.floor(itemA.count / 2)) + 1 + const itemB = splitItem(itemA, splitCount) + + for (const item of [itemA, itemB]) { + if (item.count > 1 && ctx.random.nextFloat() < 0.5) { + queue.push(item) + } else { + items.push(item) + } + } + } + + items.push(...queue) + shuffle(items, ctx) + + const results: SlottedItem[] = [] + for (const item of items) { + const slot = slots.pop() + if (slot === undefined) { + break + } + if (item.id !== 'minecraft:air' && item.count > 0) { + results.push({ slot, item }) + } + } + return results +} + +function assignSlots(items: Item[]): SlottedItem[] { + return items.map((item, i) => ({ slot: i, item })) +} + +function splitItem(item: Item, count: number): Item { + const splitCount = Math.min(count, item.count) + const other = deepClone(item) + other.count = splitCount + item.count = item.count - splitCount + return other +} + +function shuffle(array: T[], ctx: LootContext) { + let i = array.length + while (i > 0) { + const j = ctx.random.nextInt(i) + i -= 1; + [array[i], array[j]] = [array[j], array[i]] + } + return array +} + +function generateTable(table: any, consumer: ItemConsumer, ctx: LootContext) { + const tableConsumer = decorateFunctions(table.functions ?? [], consumer, ctx) + for (const pool of table.pools ?? []) { + generatePool(pool, tableConsumer, ctx) + } +} + +function createLootContext(options: LootOptions): LootContext { + return { + ...options, + random: new LegacyRandom(options.seed), + luck: options.luck, + weather: options.weather, + dayTime: options.daytime, + getItemTag: () => [], + getLootTable: () => ({ pools: [] }), + getPredicate: () => [], + } +} + +function generatePool(pool: any, consumer: ItemConsumer, ctx: LootContext) { + if (composeConditions(pool.conditions ?? [])(ctx)) { + const poolConsumer = decorateFunctions(pool.functions ?? [], consumer, ctx) + + const rolls = computeInt(pool.rolls, ctx) + Math.floor(computeFloat(pool.bonus_rolls, ctx) * ctx.luck) + for (let i = 0; i < rolls; i += 1) { + let totalWeight = 0 + const entries: any[] = [] + + // Expand entries + for (const entry of pool.entries ?? []) { + expandEntry(entry, ctx, (e) => { + const weight = computeWeight(e, ctx.luck) + if (weight > 0) { + entries.push(e) + totalWeight += weight + } + }) + } + + // Select random entry + if (totalWeight === 0 || entries.length === 0) { + continue + } + if (entries.length === 1) { + createItem(entries[0], poolConsumer, ctx) + continue + } + let remainingWeight = ctx.random.nextInt(totalWeight) + for (const entry of entries) { + remainingWeight -= computeWeight(entry, ctx.luck) + if (remainingWeight < 0) { + createItem(entry, poolConsumer, ctx) + break + } + } + } + } +} + +function expandEntry(entry: any, ctx: LootContext, consumer: (entry: any) => void): boolean { + if (!canEntryRun(entry, ctx)) { + return false + } + const type = entry.type?.replace(/^minecraft:/, '') + switch (type) { + case 'group': + for (const child of entry.children ?? []) { + expandEntry(child, ctx, consumer) + } + return true + case 'alternatives': + for (const child of entry.children ?? []) { + if (expandEntry(child, ctx, consumer)) { + return true + } + } + return false + case 'sequence': + for (const child of entry.children ?? []) { + if (!expandEntry(child, ctx, consumer)) { + return false + } + } + return true + case 'tag': + if (entry.expand) { + ctx.getItemTag(entry.tag ?? '').forEach(tagEntry => { + consumer({ type: 'item', name: tagEntry }) + }) + } else { + consumer(entry) + } + return true + default: + consumer(entry) + return true + } +} + +function canEntryRun(entry: any, ctx: LootContext): boolean { + return composeConditions(entry.conditions ?? [])(ctx) +} + +function createItem(entry: any, consumer: ItemConsumer, ctx: LootContext) { + const entryConsumer = decorateFunctions(entry.functions ?? [], consumer, ctx) + + const type = entry.type?.replace(/^minecraft:/, '') + switch (type) { + case 'item': + entryConsumer({ id: entry.name, count: 1 }) + break + case 'tag': + ctx.getItemTag(entry.name ?? '').forEach(tagEntry => { + entryConsumer({ id: tagEntry, count: 1 }) + }) + break + case 'loot_table': + generateTable(ctx.getLootTable(entry.name), entryConsumer, ctx) + break + case 'dynamic': + // not relevant for this simulation + break + } +} + +function computeWeight(entry: any, luck: number) { + return Math.max(Math.floor((entry.weight ?? 1) + (entry.quality ?? 0) * luck), 0) +} + +type LootFunction = (item: Item, ctx: LootContext) => void + +function decorateFunctions(functions: any[], consumer: ItemConsumer, ctx: LootContext): ItemConsumer { + const compositeFunction = composeFunctions(functions) + return (item) => { + compositeFunction(item, ctx) + consumer(item) + } +} + +function composeFunctions(functions: any[]): LootFunction { + return (item, ctx) => { + for (const fn of functions) { + if (composeConditions(fn.conditions ?? [])(ctx)) { + const type = fn.function?.replace(/^minecraft:/, ''); + (LootFunctions[type]?.(fn) ?? (i => i))(item, ctx) + } + } + } +} + +const LootFunctions: Record LootFunction> = { + enchant_randomly: ({ enchantments }) => (item, ctx) => { + const isBook = item.id === 'minecraft:book' + if (enchantments === undefined || enchantments.length === 0) { + enchantments = [...Enchantments.keys()] + .filter(e => { + const data = getEnchantmentData(e) + return data.discoverable && (isBook || data.canEnchant(item.id)) + }) + } + const id = enchantments[ctx.random.nextInt(enchantments.length)] + const data = getEnchantmentData(id) + const lvl = ctx.random.nextInt(data.maxLevel - data.minLevel + 1) + data.minLevel + enchantItem(item, { id, lvl }) + }, + enchant_with_levels: ({ levels, treasure }) => (item, ctx) => { + const enchants = selectEnchantments(ctx.random, item, computeInt(levels, ctx), treasure) + const isBook = item.id === 'minecraft:book' + if (isBook) { + item.id = 'minecraft:enchanted_book' + item.count = 1 + item.tag = {} + } + for (const enchant of enchants) { + enchantItem(item, enchant) + } + }, + limit_count: ({ limit }) => (item, ctx) => { + const { min, max } = prepareIntRange(limit, ctx) + item.count = clamp(item.count, min, max ) + }, + set_count: ({ count }) => (item, ctx) => { + item.count = computeInt(count, ctx) + }, + set_damage: ({ damage, add }) => (item, ctx) => { + const maxDamage = MaxDamageItems.get(item.id) + if (maxDamage) { + const oldDamage = add ? 1 - (item.tag?.Damage ?? 0) / maxDamage : 0 + const newDamage = 1 - clamp(computeFloat(damage, ctx) + oldDamage, 0, 1) + const finalDamage = Math.floor(newDamage * maxDamage) + item.tag = { ...item.tag, Damage: finalDamage } + } + }, + set_enchantments: ({ enchantments, add }) => (item, ctx) => { + Object.entries(enchantments).forEach(([id, level]) => { + const lvl = computeInt(level, ctx) + enchantItem(item, { id, lvl }, add) + }) + }, + set_lore: ({ lore, replace }) => (item) => { + const lines = lore.map((line: any) => JSON.stringify(line)) + const newLore = replace ? lines : [...(item.tag?.display?.Lore ?? []), ...lines] + item.tag = { ...item.tag, display: { ...item.tag?.display, Lore: newLore } } + }, + set_name: ({ name }) => (item) => { + const newName = JSON.stringify(name) + item.tag = { ...item.tag, display: { ...item.tag?.display, Name: newName } } + }, +} + +type LootCondition = (ctx: LootContext) => boolean + +function composeConditions(conditions: any[]): LootCondition { + return (ctx) => { + for (const cond of conditions) { + if (!testCondition(cond, ctx)) { + return false + } + } + return true + } +} + +function testCondition(condition: any, ctx: LootContext): boolean { + const type = condition.condition?.replace(/^minecraft:/, '') + return (LootConditions[type]?.(condition) ?? (() => true))(ctx) +} + +const LootConditions: Record LootCondition> = { + alternative: ({ terms }) => (ctx) => { + for (const term of terms) { + if (testCondition(term, ctx)) { + return true + } + } + return false + }, + block_state_property: () => () => { + return false // TODO + }, + damage_source_properties: ({ predicate }) => (ctx) => { + return testDamageSourcePredicate(predicate, ctx) + }, + entity_properties: ({ predicate }) => (ctx) => { + return testEntityPredicate(predicate, ctx) + }, + entity_scores: () => () => { + return false // TODO, + }, + inverted: ({ term }) => (ctx) => { + return !testCondition(term, ctx) + }, + killed_by_player: ({ inverted }) => () => { + return (inverted ?? false) === false // TODO + }, + location_check: ({ predicate }) => (ctx) => { + return testLocationPredicate(predicate, ctx) + }, + match_tool: ({ predicate }) => (ctx) => { + return testItemPredicate(predicate, ctx) + }, + random_chance: ({ chance }) => (ctx) => { + return ctx.random.nextFloat() < chance + }, + random_chance_with_looting: ({ chance, looting_multiplier }) => (ctx) => { + const level = 0 // TODO: get looting level from killer + const probability = chance + level * looting_multiplier + return ctx.random.nextFloat() < probability + + }, + reference: ({ name }) => (ctx) => { + const predicate = ctx.getPredicate(name) ?? [] + if (Array.isArray(predicate)) { + return composeConditions(predicate)(ctx) + } + return testCondition(predicate, ctx) + }, + survives_explosion: () => () => true, + table_bonus: ({ chances }) => (ctx) => { + const level = 0 // TODO: get enchantment level from tool + const chance = chances[clamp(level, 0, chances.length - 1)] + return ctx.random.nextFloat() < chance + }, + time_check: ({ value, period }) => (ctx) => { + let time = ctx.dayTime + if (period !== undefined) { + time = time % period + } + const { min, max } = prepareIntRange(value, ctx) + return min <= time && time <= max + }, + value_check: () => () => { + return false // TODO + }, + weather_check: ({ raining, thundering }) => (ctx) => { + const isRaining = ctx.weather === 'rain' || ctx.weather === 'thunder' + const isThundering = ctx.weather === 'thunder' + if (raining !== undefined && raining !== isRaining) return false + if (thundering !== undefined && thundering !== isThundering) return false + return true + }, +} + +function computeInt(provider: any, ctx: LootContext): number { + if (typeof provider === 'number') return provider + if (!isObject(provider)) return 0 + + const type = provider.type?.replace(/^minecraft:/, '') ?? 'uniform' + switch (type) { + case 'constant': + return Math.round(provider.value ?? 0) + case 'uniform': + const min = computeInt(provider.min, ctx) + const max = computeInt(provider.max, ctx) + return max < min ? min : ctx.random.nextInt(max - min + 1) + min + case 'binomial': + const n = computeInt(provider.n, ctx) + const p = computeFloat(provider.p, ctx) + let result = 0 + for (let i = 0; i < n; i += 1) { + if (ctx.random.nextFloat() < p) { + result += 1 + } + } + return result + } + return 0 +} + +function computeFloat(provider: any, ctx: LootContext): number { + if (typeof provider === 'number') return provider + if (!isObject(provider)) return 0 + + const type = provider.type?.replace(/^minecraft:/, '') ?? 'uniform' + switch (type) { + case 'constant': + return provider.value ?? 0 + case 'uniform': + const min = computeFloat(provider.min, ctx) + const max = computeFloat(provider.max, ctx) + return max < min ? min : ctx.random.nextFloat() * (max-min) + min + case 'binomial': + const n = computeInt(provider.n, ctx) + const p = computeFloat(provider.p, ctx) + let result = 0 + for (let i = 0; i < n; i += 1) { + if (ctx.random.nextFloat() < p) { + result += 1 + } + } + return result + } + return 0 +} + +function prepareIntRange(range: any, ctx: LootContext) { + if (typeof range === 'number') { + range = { min: range, max: range } + } + const min = computeInt(range.min, ctx) + const max = computeInt(range.max, ctx) + return { min, max } +} + +function testItemPredicate(_predicate: any, _ctx: LootContext) { + return false // TODO +} + +function testLocationPredicate(_predicate: any, _ctx: LootContext) { + return false // TODO +} + +function testEntityPredicate(_predicate: any, _ctx: LootContext) { + return false // TODO +} + +function testDamageSourcePredicate(_predicate: any, _ctx: LootContext) { + return false // TODO +} + +function enchantItem(item: Item, enchant: Enchant, additive?: boolean) { + if (!item.tag) { + item.tag = {} + } + const listKey = (item.id === 'minecraft:book') ? 'StoredEnchantments' : 'Enchantments' + if (!item.tag[listKey] || !Array.isArray(item.tag[listKey])) { + item.tag[listKey] = [] + } + const enchantments = item.tag[listKey] as any[] + let index = enchantments.findIndex((e: any) => e.id === enchant.id) + if (index !== -1) { + const oldEnch = enchantments[index] + oldEnch.lvl = Math.max(additive ? oldEnch.lvl + enchant.lvl : enchant.lvl, 0) + } else { + enchantments.push(enchant) + index = enchantments.length - 1 + } + if (enchantments[index].lvl === 0) { + enchantments.splice(index, 1) + } +} + +function selectEnchantments(random: Random, item: Item, levels: number, treasure: boolean): Enchant[] { + const enchantmentValue = EnchantmentItems.get(item.id) ?? 0 + if (enchantmentValue <= 0) { + return [] + } + levels += 1 + random.nextInt(Math.floor(enchantmentValue / 4 + 1)) + random.nextInt(Math.floor(enchantmentValue / 4 + 1)) + const f = (random.nextFloat() + random.nextFloat() - 1) * 0.15 + levels = clamp(Math.round(levels + levels * f), 1, Number.MAX_SAFE_INTEGER) + let available = getAvailableEnchantments(item, levels, treasure) + if (available.length === 0) { + return [] + } + const result = [] + const first = getWeightedRandom(random, available, getEnchantWeight) + if (first) result.push(first) + + while (random.nextInt(50) <= levels) { + if (result.length > 0) { + const lastAdded = result[result.length - 1] + available = available.filter(a => isEnchantCompatible(a.id, lastAdded.id)) + } + if (available.length === 0) break + const ench = getWeightedRandom(random, available, getEnchantWeight) + if (ench) result.push(ench) + levels = Math.floor(levels / 2) + } + + return result +} + +function getEnchantWeight(ench: Enchant) { + return EnchantmentsRarityWeights.get(getEnchantmentData(ench.id)?.rarity ?? 'common') ?? 0 +} + +function getAvailableEnchantments(item: Item, levels: number, treasure: boolean): Enchant[] { + const result = [] + const isBook = item.id === 'minecraft:book' + + for (const id of Enchantments.keys()) { + const ench = getEnchantmentData(id)! + if ((!ench.treasure || treasure) && ench.discoverable && (ench.canEnchant(item.id) || isBook)) { + for (let lvl = ench.maxLevel; lvl > ench.minLevel - 1; lvl -= 1) { + if (levels >= ench.minCost(lvl) && levels <= ench.maxCost(lvl)) { + result.push({ id, lvl }) + } + } + } + } + return result +} + +interface Enchant { + id: string, + lvl: number, +} + +function isEnchantCompatible(a: string, b: string) { + return a !== b && isEnchantCompatibleRaw(a, b) && isEnchantCompatibleRaw(b, a) +} + +function isEnchantCompatibleRaw(a: string, b: string) { + const ench = getEnchantmentData(a) + return ench?.isCompatible(b) +} + +export const MaxDamageItems = new Map(Object.entries({ + 'minecraft:carrot_on_a_stick': 25, + 'minecraft:warped_fungus_on_a_stick': 100, + 'minecraft:flint_and_steel': 64, + 'minecraft:elytra': 432, + 'minecraft:bow': 384, + 'minecraft:fishing_rod': 64, + 'minecraft:shears': 238, + 'minecraft:shield': 336, + 'minecraft:trident': 250, + 'minecraft:crossbow': 465, + + 'minecraft:leather_helmet': 11 * 5, + 'minecraft:leather_chestplate': 16 * 5, + 'minecraft:leather_leggings': 15 * 5, + 'minecraft:leather_boots': 13 * 5, + 'minecraft:chainmail_helmet': 11 * 15, + 'minecraft:chainmail_chestplate': 16 * 15, + 'minecraft:chainmail_leggings': 15 * 15, + 'minecraft:chainmail_boots': 13 * 15, + 'minecraft:iron_helmet': 11 * 15, + 'minecraft:iron_chestplate': 16 * 15, + 'minecraft:iron_leggings': 15 * 15, + 'minecraft:iron_boots': 13 * 15, + 'minecraft:diamond_helmet': 11 * 33, + 'minecraft:diamond_chestplate': 16 * 33, + 'minecraft:diamond_leggings': 15 * 33, + 'minecraft:diamond_boots': 13 * 33, + 'minecraft:golden_helmet': 11 * 7, + 'minecraft:golden_chestplate': 16 * 7, + 'minecraft:golden_leggings': 15 * 7, + 'minecraft:golden_boots': 13 * 7, + 'minecraft:netherite_helmet': 11 * 37, + 'minecraft:netherite_chestplate': 16 * 37, + 'minecraft:netherite_leggings': 15 * 37, + 'minecraft:netherite_boots': 13 * 37, + 'minecraft:turtle_helmet': 11 * 25, + + 'minecraft:wooden_sword': 59, + 'minecraft:wooden_shovel': 59, + 'minecraft:wooden_pickaxe': 59, + 'minecraft:wooden_axe': 59, + 'minecraft:wooden_hoe': 59, + 'minecraft:stone_sword': 131, + 'minecraft:stone_shovel': 131, + 'minecraft:stone_pickaxe': 131, + 'minecraft:stone_axe': 131, + 'minecraft:stone_hoe': 131, + 'minecraft:iron_sword': 250, + 'minecraft:iron_shovel': 250, + 'minecraft:iron_pickaxe': 250, + 'minecraft:iron_axe': 250, + 'minecraft:iron_hoe': 250, + 'minecraft:diamond_sword': 1561, + 'minecraft:diamond_shovel': 1561, + 'minecraft:diamond_pickaxe': 1561, + 'minecraft:diamond_axe': 1561, + 'minecraft:diamond_hoe': 1561, + 'minecraft:gold_sword': 32, + 'minecraft:gold_shovel': 32, + 'minecraft:gold_pickaxe': 32, + 'minecraft:gold_axe': 32, + 'minecraft:gold_hoe': 32, + 'minecraft:netherite_sword': 2031, + 'minecraft:netherite_shovel': 2031, + 'minecraft:netherite_pickaxe': 2031, + 'minecraft:netherite_axe': 2031, + 'minecraft:netherite_hoe': 2031, +})) + +const EnchantmentItems = new Map(Object.entries({ + 'minecraft:book': 1, + 'minecraft:fishing_rod': 1, + 'minecraft:trident': 1, + 'minecraft:bow': 1, + 'minecraft:crossbow': 1, + + 'minecraft:leather_helmet': 15, + 'minecraft:leather_chestplate': 15, + 'minecraft:leather_leggings': 15, + 'minecraft:leather_boots': 15, + 'minecraft:chainmail_helmet': 12, + 'minecraft:chainmail_chestplate': 12, + 'minecraft:chainmail_leggings': 12, + 'minecraft:chainmail_boots': 12, + 'minecraft:iron_helmet': 9, + 'minecraft:iron_chestplate': 9, + 'minecraft:iron_leggings': 9, + 'minecraft:iron_boots': 9, + 'minecraft:diamond_helmet': 10, + 'minecraft:diamond_chestplate': 10, + 'minecraft:diamond_leggings': 10, + 'minecraft:diamond_boots': 10, + 'minecraft:golden_helmet': 25, + 'minecraft:golden_chestplate': 25, + 'minecraft:golden_leggings': 25, + 'minecraft:golden_boots': 25, + 'minecraft:netherite_helmet': 15, + 'minecraft:netherite_chestplate': 15, + 'minecraft:netherite_leggings': 15, + 'minecraft:netherite_boots': 15, + 'minecraft:turtle_helmet': 15, + + 'minecraft:wooden_sword': 15, + 'minecraft:wooden_shovel': 15, + 'minecraft:wooden_pickaxe': 15, + 'minecraft:wooden_axe': 15, + 'minecraft:wooden_hoe': 15, + 'minecraft:stone_sword': 5, + 'minecraft:stone_shovel': 5, + 'minecraft:stone_pickaxe': 5, + 'minecraft:stone_axe': 5, + 'minecraft:stone_hoe': 5, + 'minecraft:iron_sword': 14, + 'minecraft:iron_shovel': 14, + 'minecraft:iron_pickaxe': 14, + 'minecraft:iron_axe': 14, + 'minecraft:iron_hoe': 14, + 'minecraft:diamond_sword': 10, + 'minecraft:diamond_shovel': 10, + 'minecraft:diamond_pickaxe': 10, + 'minecraft:diamond_axe': 10, + 'minecraft:diamond_hoe': 10, + 'minecraft:gold_sword': 22, + 'minecraft:gold_shovel': 22, + 'minecraft:gold_pickaxe': 22, + 'minecraft:gold_axe': 22, + 'minecraft:gold_hoe': 22, + 'minecraft:netherite_sword': 15, + 'minecraft:netherite_shovel': 15, + 'minecraft:netherite_pickaxe': 15, + 'minecraft:netherite_axe': 15, + 'minecraft:netherite_hoe': 15, +})) + +interface EnchantmentData { + id: string + rarity: 'common' | 'uncommon' | 'rare' | 'very_rare' + category: 'armor' | 'armor_feet' | 'armor_legs' | 'armor_chest' | 'armor_head' | 'weapon' | 'digger' | 'fishing_rod' | 'trident' | 'breakable' | 'bow' | 'wearable' | 'crossbow' | 'vanishable' + minLevel: number + maxLevel: number + minCost: (lvl: number) => number + maxCost: (lvl: number) => number + discoverable: boolean + treasure: boolean + curse: boolean + canEnchant: (id: string) => boolean + isCompatible: (other: string) => boolean +} + +export function getEnchantmentData(id: string): EnchantmentData { + const data = Enchantments.get(id) + const category = data?.category ?? 'armor' + return { + id, + rarity: data?.rarity ?? 'common', + category, + minLevel: data?.minLevel ?? 1, + maxLevel: data?.maxLevel ?? 1, + minCost: data?.minCost ?? ((lvl) => 1 + lvl * 10), + maxCost: data?.maxCost ?? ((lvl) => 6 + lvl * 10), + discoverable: data?.discoverable ?? true, + treasure: data?.treasure ?? false, + curse: data?.curse ?? false, + canEnchant: id => EnchantmentsCategories.get(category)!.includes(id), + isCompatible: data?.isCompatible ?? (() => true), + } +} + +const PROTECTION_ENCHANTS = ['minecraft:protection', 'minecraft:fire_protection', 'minecraft:blast_protection', 'minecraft:projectile_protection'] +const DAMAGE_ENCHANTS = ['minecraft:sharpness', 'minecraft:smite', 'minecraft:bane_of_arthropods'] + +const Enchantments = new Map(Object.entries>({ + 'minecraft:protection': { rarity: 'common', category: 'armor', maxLevel: 4, + minCost: lvl => 1 + (lvl - 1) * 11, + maxCost: lvl => 1 + (lvl - 1) * 11 + 11, + isCompatible: other => !PROTECTION_ENCHANTS.includes(other) }, + 'minecraft:fire_protection': { rarity: 'uncommon', category: 'armor', maxLevel: 4, + minCost: lvl => 10 + (lvl - 1) * 8, + maxCost: lvl => 10 + (lvl - 1) * 8 + 8, + isCompatible: other => !PROTECTION_ENCHANTS.includes(other) }, + 'minecraft:feather_falling': { rarity: 'uncommon', category: 'armor_feet', maxLevel: 4, + minCost: lvl => 5 + (lvl - 1) * 6, + maxCost: lvl => 5 + (lvl - 1) * 6 + 6 }, + 'minecraft:blast_protection': { rarity: 'rare', category: 'armor', maxLevel: 4, + minCost: lvl => 5 + (lvl - 1) * 8, + maxCost: lvl => 5 + (lvl - 1) * 8 + 8, + isCompatible: other => !PROTECTION_ENCHANTS.includes(other) }, + 'minecraft:projectile_protection': { rarity: 'uncommon', category: 'armor', maxLevel: 4, + minCost: lvl => 3 + (lvl - 1) * 6, + maxCost: lvl => 3 + (lvl - 1) * 6 + 6, + isCompatible: other => !PROTECTION_ENCHANTS.includes(other) }, + 'minecraft:respiration': { rarity: 'rare', category: 'armor_head', maxLevel: 3, + minCost: lvl => 10 * lvl, + maxCost: lvl => 10 * lvl + 30 }, + 'minecraft:aqua_affinity': { rarity: 'rare', category: 'armor_head', + minCost: () => 1, + maxCost: () => 40 }, + 'minecraft:thorns': { rarity: 'very_rare', category: 'armor_chest', maxLevel: 3, + minCost: lvl => 10 + 20 * (lvl - 1), + maxCost: lvl => 10 + 20 * (lvl - 1) + 50 }, + 'minecraft:depth_strider': { rarity: 'rare', category: 'armor_feet', maxLevel: 3, + minCost: lvl => 10 * lvl, + maxCost: lvl => 10 * lvl + 15, + isCompatible: other => other !== 'minecraft:frost_walker' }, + 'minecraft:frost_walker': { rarity: 'rare', category: 'armor_feet', maxLevel: 2, treasure: true, + minCost: lvl => 10 * lvl, + maxCost: lvl => 10 * lvl + 15, + isCompatible: other => other !== 'minecraft:depth_strider' }, + 'minecraft:binding_curse': { rarity: 'very_rare', category: 'wearable', treasure: true, curse: true, + minCost: () => 25, + maxCost: () => 50 }, + 'minecraft:soul_speed': { rarity: 'very_rare', category: 'armor_feet', maxLevel: 3, + discoverable: false, treasure: true, + minCost: lvl => 10 * lvl, + maxCost: lvl => 10 * lvl + 15 }, + 'minecraft:swift_sneak': { rarity: 'very_rare', category: 'armor_legs', maxLevel: 3, + discoverable: false, treasure: true, + minCost: lvl => 25 * lvl, + maxCost: lvl => 25 * lvl + 50 }, + 'minecraft:sharpness': { rarity: 'common', category: 'weapon', maxLevel: 5, + minCost: lvl => 1 + (lvl - 1) * 11, + maxCost: lvl => 1 + (lvl - 1) * 11 + 20, + isCompatible: other => !DAMAGE_ENCHANTS.includes(other) }, + 'minecraft:smite': { rarity: 'common', category: 'weapon', maxLevel: 5, + minCost: lvl => 5 + (lvl - 1) * 8, + maxCost: lvl => 5 + (lvl - 1) * 8 + 20, + isCompatible: other => !DAMAGE_ENCHANTS.includes(other) }, + 'minecraft:bane_of_arthropods': { rarity: 'common', category: 'weapon', maxLevel: 5, + minCost: lvl => 5 + (lvl - 1) * 8, + maxCost: lvl => 5 + (lvl - 1) * 8 + 20, + isCompatible: other => !DAMAGE_ENCHANTS.includes(other) }, + 'minecraft:knockback': { rarity: 'uncommon', category: 'weapon', maxLevel: 2, + minCost: lvl => 5 + 20 * (lvl - 1), + maxCost: lvl => 1 + lvl * 10 + 50 }, + 'minecraft:fire_aspect': { rarity: 'rare', category: 'weapon', maxLevel: 2, + minCost: lvl => 5 + 20 * (lvl - 1), + maxCost: lvl => 1 + lvl * 10 + 50 }, + 'minecraft:looting': { rarity: 'rare', category: 'weapon', maxLevel: 3, + minCost: lvl => 15 + (lvl - 1) * 9, + maxCost: lvl => 1 + lvl * 10 + 50, + isCompatible: other => other !== 'minecraft:silk_touch' }, + 'minecraft:sweeping': { rarity: 'rare', category: 'weapon', maxLevel: 3, + minCost: lvl => 5 + (lvl - 1) * 9, + maxCost: lvl => 5 + (lvl - 1) * 9 + 15 }, + 'minecraft:efficiency': { rarity: 'common', category: 'digger', maxLevel: 5, + minCost: lvl => 1 + 10 * (lvl - 1), + maxCost: lvl => 1 + lvl * 10 + 50, + canEnchant: id => id === 'minecraft:shears' || EnchantmentsCategories.get('digger')!.includes(id) }, + 'minecraft:silk_touch': { rarity: 'very_rare', category: 'digger', + minCost: () => 15, + maxCost: lvl => 1 + lvl * 10 + 50, + isCompatible: other => other !== 'minecraft:fortune' }, + 'minecraft:unbreaking': { rarity: 'uncommon', category: 'breakable', maxLevel: 3, + minCost: lvl => 5 + (lvl - 1) * 8, + maxCost: lvl => 1 + lvl * 10 + 50 }, + 'minecraft:fortune': { rarity: 'rare', category: 'digger', maxLevel: 3, + minCost: lvl => 15 + (lvl - 1) * 9, + maxCost: lvl => 1 + lvl * 10 + 50, + isCompatible: other => other !== 'minecraft:silk_touch' }, + 'minecraft:power': { rarity: 'common', category: 'bow', maxLevel: 5, + minCost: lvl => 1 + (lvl - 1) * 10, + maxCost: lvl => 1 + (lvl - 1) * 10 + 15 }, + 'minecraft:punch': { rarity: 'rare', category: 'bow', maxLevel: 2, + minCost: lvl => 12 + (lvl - 1) * 20, + maxCost: lvl => 12 + (lvl - 1) * 20 + 25 }, + 'minecraft:flame': { rarity: 'rare', category: 'bow', + minCost: () => 20, + maxCost: () => 50 }, + 'minecraft:infinity': { rarity: 'very_rare', category: 'bow', + minCost: () => 20, + maxCost: () => 50, + isCompatible: other => other !== 'minecraft:mending' }, + 'minecraft:luck_of_the_sea': { rarity: 'rare', category: 'fishing_rod', maxLevel: 3, + minCost: lvl => 15 + (lvl - 1) * 9, + maxCost: lvl => 1 + lvl * 10 + 50, + isCompatible: other => other !== 'minecraft:silk_touch' }, + 'minecraft:lure': { rarity: 'rare', category: 'fishing_rod', maxLevel: 3, + minCost: lvl => 15 + (lvl - 1) * 9, + maxCost: lvl => 1 + lvl * 10 + 50 }, + 'minecraft:loyalty': { rarity: 'uncommon', category: 'trident', maxLevel: 3, + minCost: lvl => 5 + lvl * 7, + maxCost: () => 50 }, + 'minecraft:impaling': { rarity: 'rare', category: 'trident', maxLevel: 5, + minCost: lvl => 1 + (lvl - 1) * 8, + maxCost: lvl => 1 + (lvl - 1) * 8 + 20 }, + 'minecraft:riptide': { rarity: 'rare', category: 'trident', maxLevel: 3, + minCost: lvl => 5 + lvl * 7, + maxCost: () => 50, + isCompatible: other => !['minecraft:riptide', 'minecraft:channeling'].includes(other) }, + 'minecraft:channeling': { rarity: 'very_rare', category: 'trident', + minCost: () => 25, + maxCost: () => 50 }, + 'minecraft:multishot': { rarity: 'rare', category: 'crossbow', + minCost: () => 20, + maxCost: () => 50, + isCompatible: other => other !== 'minecraft:piercing' }, + 'minecraft:quick_charge': { rarity: 'uncommon', category: 'crossbow', maxLevel: 3, + minCost: lvl => 12 + (lvl - 1) * 20, + maxCost: () => 50 }, + 'minecraft:piercing': { rarity: 'common', category: 'crossbow', maxLevel: 4, + minCost: lvl => 1 + (lvl - 1) * 10, + maxCost: () => 50, + isCompatible: other => other !== 'minecraft:multishot' }, + 'minecraft:mending': { rarity: 'rare', category: 'breakable', treasure: true, + minCost: lvl => lvl * 25, + maxCost: lvl => lvl * 25 + 50 }, + 'minecraft:vanishing_curse': { rarity: 'very_rare', category: 'vanishable', treasure: true, curse: true, + minCost: () => 25, + maxCost: () => 50 }, +})) + +const EnchantmentsRarityWeights = new Map(Object.entries({ + common: 10, + uncommon: 5, + rare: 2, + very_rare: 1, +})) + +const ARMOR_FEET = [ + 'minecraft:leather_boots', + 'minecraft:chainmail_boots', + 'minecraft:iron_boots', + 'minecraft:diamond_boots', + 'minecraft:golden_boots', + 'minecraft:netherite_boots', +] +const ARMOR_LEGS = [ + 'minecraft:leather_leggings', + 'minecraft:chainmail_leggings', + 'minecraft:iron_leggings', + 'minecraft:diamond_leggings', + 'minecraft:golden_leggings', + 'minecraft:netherite_leggings', +] +const ARMOR_CHEST = [ + 'minecraft:leather_chestplate', + 'minecraft:chainmail_chestplate', + 'minecraft:iron_chestplate', + 'minecraft:diamond_chestplate', + 'minecraft:golden_chestplate', + 'minecraft:netherite_chestplate', +] +const ARMOR_HEAD = [ + 'minecraft:leather_helmet', + 'minecraft:chainmail_helmet', + 'minecraft:iron_helmet', + 'minecraft:diamond_helmet', + 'minecraft:golden_helmet', + 'minecraft:netherite_helmet', + 'minecraft:turtle_helmet', +] +const ARMOR = [...ARMOR_FEET, ...ARMOR_LEGS, ...ARMOR_CHEST, ...ARMOR_HEAD] +const SWORD = [ + 'minecraft:wooden_sword', + 'minecraft:stone_sword', + 'minecraft:iron_sword', + 'minecraft:diamond_sword', + 'minecraft:gold_sword', + 'minecraft:netherite_sword', +] +const DIGGER = [ + 'minecraft:wooden_shovel', + 'minecraft:wooden_pickaxe', + 'minecraft:wooden_axe', + 'minecraft:wooden_hoe', + 'minecraft:stone_shovel', + 'minecraft:stone_pickaxe', + 'minecraft:stone_axe', + 'minecraft:stone_hoe', + 'minecraft:iron_shovel', + 'minecraft:iron_pickaxe', + 'minecraft:iron_axe', + 'minecraft:iron_hoe', + 'minecraft:diamond_shovel', + 'minecraft:diamond_pickaxe', + 'minecraft:diamond_axe', + 'minecraft:diamond_hoe', + 'minecraft:gold_shovel', + 'minecraft:gold_pickaxe', + 'minecraft:gold_axe', + 'minecraft:gold_hoe', + 'minecraft:netherite_shovel', + 'minecraft:netherite_pickaxe', + 'minecraft:netherite_axe', + 'minecraft:netherite_hoe', +] +const BREAKABLE = [...MaxDamageItems.keys()] +const WEARABLE = [ + ...ARMOR, + 'minecraft:elytra', + 'minecraft:carved_pumpkin', + 'minecraft:creeper_head', + 'minecraft:dragon_head', + 'minecraft:player_head', + 'minecraft:zombie_head', +] + +const EnchantmentsCategories = new Map(Object.entries({ + armor: ARMOR, + armor_feet: ARMOR_FEET, + armor_legs: ARMOR_LEGS, + armor_chest: ARMOR_CHEST, + armor_head: ARMOR_HEAD, + weapon: SWORD, + digger: DIGGER, + fishing_rod: ['minecraft:fishing_rod'], + trident: ['minecraft:trident'], + breakable: BREAKABLE, + bow: ['minecraft:bow'], + wearable: WEARABLE, + crossbow: ['minecraft:crossbow'], + vanishable: [...BREAKABLE, 'minecraft:compass'], +})) diff --git a/src/app/schema/renderHtml.tsx b/src/app/schema/renderHtml.tsx index 83568f87..b35cb201 100644 --- a/src/app/schema/renderHtml.tsx +++ b/src/app/schema/renderHtml.tsx @@ -139,7 +139,7 @@ const renderHtml: RenderHook = { let label: undefined | string | JSX.Element if (['loot_pool.entries.entry', 'loot_entry.alternatives.children.entry', 'loot_entry.group.children.entry', 'loot_entry.sequence.children.entry', 'function.set_contents.entries.entry'].includes(cPath.getContext().join('.'))) { if (isObject(cValue) && typeof cValue.type === 'string' && cValue.type.replace(/^minecraft:/, '') === 'item' && typeof cValue.name === 'string') { - label = + label = } } diff --git a/src/app/services/DataFetcher.ts b/src/app/services/DataFetcher.ts index bf003a9a..eacd7032 100644 --- a/src/app/services/DataFetcher.ts +++ b/src/app/services/DataFetcher.ts @@ -204,6 +204,16 @@ async function loadImage(src: string) { } */ +export async function fetchLanguage(versionId: VersionId, lang: string = 'en_us') { + const version = config.versions.find(v => v.id === versionId)! + await validateCache(version) + try { + return await cachedFetch>(`${mcmeta(version, 'assets')}/assets/minecraft/lang/${lang}.json`) + } catch (e) { + throw new Error(`Error occured while fetching language: ${message(e)}`) + } +} + export interface Change { group: string, version: string, diff --git a/src/app/services/Resources.ts b/src/app/services/Resources.ts index 22cfaacd..a8b82ad6 100644 --- a/src/app/services/Resources.ts +++ b/src/app/services/Resources.ts @@ -1,7 +1,7 @@ import type { BlockModelProvider, TextureAtlasProvider, UV } from 'deepslate/render' import { BlockModel, Identifier, ItemRenderer, TextureAtlas, upperPowerOfTwo } from 'deepslate/render' import { message } from '../Utils.js' -import { fetchResources } from './DataFetcher.js' +import { fetchLanguage, fetchResources } from './DataFetcher.js' import type { VersionId } from './Schemas.js' const Resources: Record> = {} @@ -99,3 +99,83 @@ export class ResourceManager implements BlockModelProvider, TextureAtlasProvider this.textureAtlas = new TextureAtlas(imageData, idMap) } } + +const Languages: Record | Promise>> = {} + +export async function getLanguage(version: VersionId) { + if (!Languages[version]) { + Languages[version] = (async () => { + try { + Languages[version] = await fetchLanguage(version) + return Languages[version] + } catch (e) { + console.error('Error: ', e) + throw new Error(`Cannot get language for version ${version}: ${message(e)}`) + } + })() + return Languages[version] + } + return Languages[version] +} + +export async function getTranslation(version: VersionId, key: string, params?: string[]) { + const lang = await getLanguage(version) + const str = lang[key] + if (!str) return null + return replaceTranslation(str, params) +} + +export function replaceTranslation(src: string, params?: string[]) { + let out = '' + let i = 0 + let p = 0 + while (i < src.length) { + const c0 = src[i++] + if (c0 === '%') { // percent character + if (i >= src.length) { // INVALID: % + out += c0 + break + } + let c1 = src[i++] + if (c1 === '%') { // escape + out += '%' + } else if (c1 === 's' || c1 === 'd') { // short form %s + out += params?.[p++] ?? '' + } else if (c1 >= '0' && c1 <= '9') { + if (i >= src.length) { // INVALID: %2 + out += c0 + c1 + break + } + let num = '' + do { + num += c1 + c1 = src[i++] + } while (i < src.length && c1 >= '0' && c1 <= '9') + if (c1 === '$') { + if (i >= src.length) { // INVALID: %2$ + out += c0 + num + c1 + break + } + const c2 = src[i++] + if (c2 === 's' || c2 === 'd') { // long form %2$s + const pos = parseInt(num) - 1 + if (!params || isNaN(pos) || pos < 0 || pos >= params.length) { + out += '' + } else { + out += params[pos] + } + } else { // INVALID: %2$... + out += c0 + num + c1 + } + } else { // INVALID: %2... + out += c0 + num + } + } else { // INVALID: %... + out += c0 + } + } else { // normal character + out += c0 + } + } + return out +} diff --git a/src/locales/en.json b/src/locales/en.json index 5138fdd3..b4bc585e 100644 --- a/src/locales/en.json +++ b/src/locales/en.json @@ -127,11 +127,17 @@ "preview": "Visualize", "preview.auto_scroll": "Auto scroll", "preview.biome": "Biome", + "preview.daytime": "Daytime", + "preview.luck": "Luck", "preview.scale": "Scale", "preview.depth": "Depth", "preview.factor": "Factor", "preview.offset": "Offset", "preview.peaks": "Peaks", + "preview.weather": "Weather", + "preview.weather.clear": "Clear", + "preview.weather.rain": "Rain", + "preview.weather.thunder": "Thunder", "preview.width": "Width", "project.new": "New project", "project.cancel": "Cancel", diff --git a/src/styles/global.css b/src/styles/global.css index 5ffe44a9..fa0eb96b 100644 --- a/src/styles/global.css +++ b/src/styles/global.css @@ -132,6 +132,7 @@ body { min-height: 100vh; overflow-x: hidden; background-color: var(--background-1); + --full-width: calc(100vw - (100vw - 100%)); } header { @@ -454,6 +455,10 @@ main.has-preview { background-color: var(--nav-faded); display: block; cursor: crosshair; +} + +.popup-preview canvas, +.popup-preview .pixelated { image-rendering: -moz-crisp-edges; image-rendering: -webkit-crisp-edges; image-rendering: crisp-edges; @@ -524,6 +529,20 @@ main.has-project { padding-left: max(200px, 20vw); } +.preview-overlay { + height: min-content; + position: relative; +} + +.preview-overlay > img { + display: block; + width: 100%; +} + +.preview-overlay > div { + position: absolute; +} + .btn { display: flex; align-items: center; @@ -669,7 +688,8 @@ main.has-project { padding-right: 7px; } -.btn-input input { +.btn-input input, +.btn-input select { background: var(--background-1); color: var(--text-1); font-size: 17px; @@ -679,7 +699,8 @@ main.has-project { width: 100px; } -.btn-input.larger-input input { +.btn-input.larger-input input, +.btn-input.larger-input select { width: 200px; } @@ -688,7 +709,8 @@ main.has-project { padding-left: 11px; } -.btn-input.large-input input { +.btn-input.large-input input, +.btn-input.large-input select { width: 100%; height: 100%; } @@ -1164,29 +1186,149 @@ hr { } .item-display { - width: 32px; - height: 32px; + position: relative; + width: 100%; + height: 100%; display: flex; align-items: center; justify-content: center; } .item-display > img { - width: 26px; - position: relative; - image-rendering: pixelated; + width: 88.888%; + image-rendering: -moz-crisp-edges; + image-rendering: -webkit-crisp-edges; + image-rendering: crisp-edges; + image-rendering: pixelated; } -.item-display > svg { - width: 26px; - height: 20px; - position: relative; +.item-display > img.model { + image-rendering: auto; +} + +.item-display > svg:not(.item-count):not(.item-durability) { + width: 81.25%; + height: 62.5%; fill: var(--node-text-dimmed); } -.item-display > canvas { - width: 32px; - height: 32px; +.item-display > svg.item-count, +.item-display > svg.item-durability, +.item-display > .item-glint, +.item-display > .item-slot-overlay { + position: absolute; + right: 0; + bottom: 0; + width: 100%; + height: 100%; + user-select: none; + -webkit-user-select: none; + -moz-user-select: none; + -ms-user-select: none; +} + +.item-display > .item-glint, +.item-display > .item-slot-overlay { + left: 5.555%; + top: 5.555%; + width: 88.888%; + height: 88.888%; +} + +.item-display > .item-glint, +.item-display > .item-glint::after { + background: url(/images/glint.png) repeat; + filter: brightness(1.4) blur(1px) opacity(0.8); + animation: glint 20s linear 0s infinite; + background-size: 400%; + background-blend-mode: overlay; + -webkit-mask-image: var(--mask-image); + mask-image: var(--mask-image); + -webkit-mask-size: contain; + mask-size: contain; + image-rendering: -moz-crisp-edges; + image-rendering: -webkit-crisp-edges; + image-rendering: crisp-edges; + image-rendering: pixelated; +} + +.item-display > .item-glint::after { + content: ''; + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; + animation: glint2 30s linear 0s infinite; +} + +@keyframes glint { + from { + background-position: 0 0; + } + to { + background-position: -400% 400%; + } +} + +@keyframes glint2 { + from { + background-position: 100% 0; + } + to { + background-position: 500% 0; + } +} + +.item-display:hover > .item-slot-overlay { + background-color: #fff4; +} + +.item-tooltip { + padding: 3px 1px 1px 3px; + border: solid 4px #220044; + border-image-source: url(/images/tooltip.png); + border-image-slice: 2 fill; + border-image-outset: 2px; + image-rendering: -moz-crisp-edges; + image-rendering: -webkit-crisp-edges; + image-rendering: crisp-edges; + image-rendering: pixelated; +} + +.item-display > .item-tooltip { + display: none; + position: absolute; + margin: 4px; + pointer-events: none; + z-index: 5; +} + +.item-display:hover > .item-tooltip { + display: block; +} + +.item-display > .item-tooltip > :nth-child(1) { + margin-top: -2px; +} + +.item-display > .item-tooltip > :nth-child(2) { + margin-top: 4px; +} + +.text-component { + font-family: MinecraftSeven, sans-serif; + font-size: 20px; + position: relative; + white-space: nowrap; + line-height: 1.1 ; +} + +.text-component > .text-foreground { + position: absolute; + z-index: 1; + left: -2px; + top: -2px; } .file-view { @@ -1442,12 +1584,14 @@ hr { .sound-config input[type=range] { -webkit-appearance: none; + appearance: none; width: 100%; background: transparent; } .sound-config input[type=range]::-webkit-slider-thumb { -webkit-appearance: none; + appearance: none; } .sound-config input[type=range]:focus { @@ -1456,6 +1600,7 @@ hr { .sound-config input[type=range]::-webkit-slider-thumb { -webkit-appearance: none; + appearance: none; border: none; height: 16px; width: 16px; @@ -2191,7 +2336,7 @@ hr { } .popup-source { - width: 100vw; + width: var(--full-width); } .source { @@ -2199,7 +2344,7 @@ hr { } .popup-preview { - width: 100vw; + width: var(--full-width); height: unset; bottom: 0; background-color: transparent; @@ -2223,3 +2368,8 @@ hr { display: none; } } + +@font-face { + font-family: "MinecraftSeven"; + src: url("/fonts/seven.ttf") format("truetype"); +} diff --git a/src/styles/nodes.css b/src/styles/nodes.css index 0699d33b..4ba722b0 100644 --- a/src/styles/nodes.css +++ b/src/styles/nodes.css @@ -108,6 +108,11 @@ background-color: var(--node-background-label); } +.node-header > label > .item-display { + width: 32px; + height: 32px; +} + .node-header > input { font-size: 18px; padding-left: 9px; diff --git a/vite.config.js b/vite.config.js index cbcf5c15..01a958a6 100644 --- a/vite.config.js +++ b/vite.config.js @@ -83,8 +83,6 @@ export default defineConfig({ preact(), viteStaticCopy({ targets: [ - { src: 'src/.nojekyll', dest: '' }, - { src: 'src/sitemap.txt', dest: '' }, { src: 'src/styles/giscus.css', dest: 'assets' }, { src: 'src/styles/giscus-burn.css', dest: 'assets' }, { src: 'src/guides/*', dest: 'guides' },