From c87ede8e5486e87fadf537eff52921d587eb73a1 Mon Sep 17 00:00:00 2001 From: Misode Date: Mon, 25 Mar 2024 21:57:47 +0100 Subject: [PATCH] Recipe preview --- public/images/crafting_table.png | Bin 0 -> 461 bytes public/images/furnace.png | Bin 0 -> 508 bytes public/images/smithing.png | Bin 0 -> 561 bytes public/images/stonecutter.png | Bin 0 -> 438 bytes src/app/components/generator/PreviewPanel.tsx | 8 +- .../components/previews/LootTablePreview.tsx | 11 +- src/app/components/previews/RecipePreview.tsx | 211 ++++++++++++++++++ src/app/components/previews/index.ts | 1 + 8 files changed, 221 insertions(+), 10 deletions(-) create mode 100644 public/images/crafting_table.png create mode 100644 public/images/furnace.png create mode 100644 public/images/smithing.png create mode 100644 public/images/stonecutter.png create mode 100644 src/app/components/previews/RecipePreview.tsx diff --git a/public/images/crafting_table.png b/public/images/crafting_table.png new file mode 100644 index 0000000000000000000000000000000000000000..1beddd8eb569451bb99b513cad1048c0ea1e54b0 GIT binary patch literal 461 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@+=jq}YlHvUJ_HDi<1A&%@T9>}pr`%3WStwv|gjd%1 zKkw6kPo6s%goDm?E|1w_XyR4f0XCJ)EP{BM2Z{4lY};OSFNUAIiu^pbrnC{`F8Gq3v=0S z!5ZC0-9}p^yf?Vuj`Z=Zeh2reLOqi6U}017D(_jZ#N#=W+fIF5JAcK;DW9d?MJI$# TwcpeV3{D15S3j3^P6Nn{1`MFV_7T!Hle|NoC2I~E!mYHn`c-Q5ioN!>hM8c4C11o;I6Wr2V}%A45@ zD8yOd5n0T@z;_sg8IR|$NC8?B>*?YclHvUJc4J>zos0&o@P^r(59Q>&ol8flhBOh$Q|M zd*(CY!Bdvw3DHYrUUQs!6+Odv-NE;dcdalz{-FDe^^LGqhhvwrK0Z|Ky=GPUQxTr@ zd*xOv=kMQ0RF!C~FIao;y6?~Y&Z-;NeJkP~dW+PE&3moKSMGXRU*h$ixqmdd>|SRT z{Lk5ah;x4t$3AxyZ)}r382mdKI;Vst05CAxxc~qF literal 0 HcmV?d00001 diff --git a/public/images/smithing.png b/public/images/smithing.png new file mode 100644 index 0000000000000000000000000000000000000000..a0812733c2fba8b5a40162c85deb0c0e6ecbae79 GIT binary patch literal 561 zcmeAS@N?(olHy`uVBq!ia0vp^8-O^FgBeJ=7T)m&QjEnx?oJHr&dIz4a#+$GeH|GX zHuiJ>Nn{1`EdzW)T!Hle|NoC2I~E!m%Er#Cr*9D$6s@4BndNF?Yv&ObklB zSEK+PmE`H-7?R=q_V(R%%?bi;flC9lrCa05a9DOb5>tax8K={AFVp3WrABbT+Z z7=221@*H;B7svZ&{l1(|#Vg+*uXukon|aR3?X%5_v>*8FyMH`Tc&Dhr#m5u3x2kPw zT)MicbKZH6IVTyaC3t+kROz}X#oiG+;PSrQ^UBeq?D;!?9b%ile)hEq2RUL&Hq_~v zNhmXWrzgDR(V5%0RQtgETw(d*sWZP!nfqh=PUrHg+6ON$yF3xZuiL$P9#6&QCWZ%F zB^dVzIX8G4vh5IPZrB{5Jj4I`M^l*(N^OjvO{YF6{cLL2$oSc!h4EP;57U{&B1|*b ziy0E^iku!~{`;MA5oTH(E-t#o5|MT=(L$EOuN0TK6qbV~W%`_C8< XxjVl50kbtQ3K%?H{an^LB{Ts5{>boZ literal 0 HcmV?d00001 diff --git a/public/images/stonecutter.png b/public/images/stonecutter.png new file mode 100644 index 0000000000000000000000000000000000000000..f3063b0c5d26bb33ca2484607434f3ca62c4809b GIT binary patch literal 438 zcmeAS@N?(olHy`uVBq!ia0vp^8-O^FgBeJ=7T)m&QjEnx?oJHr&dIz4a#+$GeH|GX zHuiJ>Nn{1`r2~9IT!Hle|NoC2I~E!mYHn^G=xNp6-JPB20~BApUAzNGah3%61q0Q9 z0Yk8Vg%C)vz$3Dlfr0NZ2s0kfUy%Z|jNjA6F(kwJ?d^wsO$Gw45A`^{{hfcKY;V`L z*=ID&PQRJ=zF%P0NzQ{yj!h`?D|`|(`*OI<{q5n$6@8xmyY@h{t5Wa0>w>tZcI66_ zCU@nEE&qjTZe>@>ZOmHt_U?n!<)2HU9%M1+P}|{XaFDmjUS?0+|JU^k zxQliqa37RfeVOl|{%^}2|6kpSSucK^t4!fWX9DxVS=q`+wDXP!Pxpjx{(d#)_UwcE z+aFxykn4SLaS`izQ;X%lB;9HsYrnK;Dyo02omKdxhVci}`pUSXFQ } + if (id === 'recipe') { + return + } + if (id === 'dimension' && model.get(new Path(['generator', 'type']))?.endsWith('noise')) { return } diff --git a/src/app/components/previews/LootTablePreview.tsx b/src/app/components/previews/LootTablePreview.tsx index affe3f9c..af522469 100644 --- a/src/app/components/previews/LootTablePreview.tsx +++ b/src/app/components/previews/LootTablePreview.tsx @@ -1,11 +1,10 @@ import { DataModel } from '@mcschema/core' -import { useEffect, useRef, useState } from 'preact/hooks' +import { useMemo, useRef, useState } from 'preact/hooks' import { useLocale, useVersion } from '../../contexts/index.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' -import type { SlottedItem } from './LootTable.js' import { generateLootTable } from './LootTable.js' export const LootTablePreview = ({ data }: PreviewProps) => { @@ -19,14 +18,10 @@ export const LootTablePreview = ({ data }: PreviewProps) => { 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' }) - console.log('Generated loot', items) - setItems(items) + const items = useMemo(() => { + return generateLootTable(table, { version, seed, luck, daytime, weather, stackMixer: mixItems ? 'container' : 'default' }) }, [version, seed, luck, daytime, weather, mixItems, state]) return <> diff --git a/src/app/components/previews/RecipePreview.tsx b/src/app/components/previews/RecipePreview.tsx new file mode 100644 index 00000000..6e0a5dcc --- /dev/null +++ b/src/app/components/previews/RecipePreview.tsx @@ -0,0 +1,211 @@ +import { DataModel } from '@mcschema/core' +import { Identifier, ItemStack } from 'deepslate' +import { useEffect, useMemo, useRef, useState } from 'preact/hooks' +import { useLocale, useVersion } from '../../contexts/index.js' +import { useAsync } from '../../hooks/useAsync.js' +import { fetchAllPresets } from '../../services/index.js' +import { Btn, BtnMenu } from '../index.js' +import { ItemDisplay } from '../ItemDisplay.jsx' +import type { PreviewProps } from './index.js' + +const ANIMATION_TIME = 1000 + +export const RecipePreview = ({ data }: PreviewProps) => { + const { locale } = useLocale() + const { version } = useVersion() + const [advancedTooltips, setAdvancedTooltips] = useState(true) + const [animation, setAnimation] = useState(0) + const overlay = useRef(null) + + const { value: itemTags } = useAsync(() => { + return fetchAllPresets(version, 'tag/item') + }, [version]) + + useEffect(() => { + const interval = setInterval(() => { + setAnimation(n => n + 1) + }, ANIMATION_TIME) + return () => clearInterval(interval) + }, []) + + const recipe = DataModel.unwrapLists(data) + const state = JSON.stringify(recipe) + const items = useMemo>(() => { + return placeItems(recipe, animation, itemTags ?? new Map()) + }, [state, animation, itemTags]) + + const gui = useMemo(() => { + const type = recipe.type?.replace(/^minecraft:/, '') + if (type === 'smelting' || type === 'blasting' || type === 'smoking' || type === 'campfire_cooking') { + return '/images/furnace.png' + } else if (type === 'stonecutting') { + return '/images/stonecutter.png' + } else if (type === 'smithing_transform' || type === 'smithing_trim') { + return '/images/smithing.png' + } else { + return '/images/crafting_table.png' + } + }, [state]) + + return <> +
+ Crafting GUI + {[...items.entries()].map(([slot, item]) => +
+ +
+ )} +
+
+ + {setAdvancedTooltips(!advancedTooltips); e.stopPropagation()}} /> + +
+ +} + +const GUI_WIDTH = 176 +const GUI_HEIGHT = 81 +const SLOT_SIZE = 18 +const SLOTS = { + 'crafting.0': [29, 16], + 'crafting.1': [47, 16], + 'crafting.2': [65, 16], + 'crafting.3': [29, 34], + 'crafting.4': [47, 34], + 'crafting.5': [65, 34], + 'crafting.6': [29, 52], + 'crafting.7': [47, 52], + 'crafting.8': [65, 52], + 'crafting.result': [123, 34], + 'smelting.ingredient': [55, 16], + 'smelting.fuel': [55, 53], + 'smelting.result': [115, 34], + 'stonecutting.ingredient': [19, 32], + 'stonecutting.result': [142, 32], + 'smithing.template': [7, 47], + 'smithing.base': [25, 47], + 'smithing.addition': [43, 47], + 'smithing.result': [97, 47], +} +type Slot = keyof typeof SLOTS + +function slotStyle(slot: Slot) { + const [x, y] = SLOTS[slot] + return { + left: `${x*100/GUI_WIDTH}%`, + top: `${y*100/GUI_HEIGHT}%`, + width: `${SLOT_SIZE*100/GUI_WIDTH}%`, + height: `${SLOT_SIZE*100/GUI_HEIGHT}%`, + } +} + +function placeItems(recipe: any, animation: number, itemTags: Map) { + const items = new Map() + const type: string = recipe.type?.replace(/^minecraft:/, '') + if (type.startsWith('crafting_special') || type === 'crafting_decorated_pot') { + return items + } + + if (type === 'crafting_shapeless') { + const ingredients: any[] = Array.isArray(recipe.ingredients) ? recipe.ingredients : [] + ingredients.forEach((ingredient, i) => { + const choices = allIngredientChoices(ingredient, itemTags) + if (i >= 0 && i < 9 && choices.length > 0) { + const choice = choices[(3 * i + animation) % choices.length] + items.set(`crafting.${i}` as Slot, choice) + } + }) + } else if (type === 'crafting_shaped') { + const keys = new Map() + for (const [key, ingredient] of Object.entries(recipe.key ?? {})) { + const choices = allIngredientChoices(ingredient, itemTags) + console.log(choices) + if (choices.length > 0) { + const choice = choices[animation % choices.length] + keys.set(key, choice) + } + } + const pattern = recipe.pattern + for (let row = 0; row < Math.min(3, pattern.length); row += 1) { + for (let col = 0; col < Math.min(3, pattern[row].length); col += 1) { + const key = pattern[row].split('')[col] + const choice = key === ' ' ? undefined : keys.get(key) + if (choice) { + items.set(`crafting.${row * 3 + col}` as Slot, choice) + } + } + } + } else if (type === 'smelting' || type === 'smoking' || type === 'blasting' || type === 'campfire_cooking') { + const choices = allIngredientChoices(recipe.ingredient, itemTags) + if (choices.length > 0) { + const choice = choices[animation % choices.length] + items.set('smelting.ingredient' as Slot, choice) + } + } else if (type === 'stonecutting') { + const choices = allIngredientChoices(recipe.ingredient, itemTags) + if (choices.length > 0) { + const choice = choices[animation % choices.length] + items.set('stonecutting.ingredient' as Slot, choice) + } + } else if (type === 'smithing_transform' || type === 'smithing_trim') { + for (const ingredient of ['template', 'base', 'addition'] as const) { + const choices = allIngredientChoices(recipe[ingredient], itemTags) + if (choices.length > 0) { + const choice = choices[animation % choices.length] + items.set(`smithing.${ingredient}`, choice) + } + } + } + + let resultSlot: Slot = 'crafting.result' + if (type === 'smelting' || type === 'smoking' || type === 'blasting' || type === 'campfire_cooking') { + resultSlot = 'smelting.result' + } else if (type === 'stonecutting') { + resultSlot = 'stonecutting.result' + } else if (type === 'smithing_transform' || type === 'smithing_trim') { + resultSlot = 'smithing.result' + } + const result = recipe.result + if (type === 'smithing_trim') { + const base = items.get('smithing.base') + if (base) { + items.set(resultSlot, base) + } + } else if (typeof result === 'string') { + items.set(resultSlot, new ItemStack(Identifier.parse(result), 1)) + } else if (typeof result === 'object' && result !== null) { + const id = typeof result.id === 'string' ? result.id : 'minecraft:air' + const count = typeof result.count === 'number' ? result.count : 1 + // TODO: add components + items.set(resultSlot, new ItemStack(Identifier.parse(id), count)) + } + + return items +} + +function allIngredientChoices(ingredient: any, itemTags: Map): ItemStack[] { + if (Array.isArray(ingredient)) { + return ingredient.flatMap(i => allIngredientChoices(i, itemTags)) + } + if (typeof ingredient === 'object' && ingredient !== null) { + if (typeof ingredient.item === 'string') { + try { + return [new ItemStack(Identifier.parse(ingredient.item), 1)] + } catch (e) {} + } else if (typeof (ingredient.tag === 'string')) { + const tag: any = itemTags.get(ingredient.tag.replace(/^minecraft:/, '')) + if (typeof tag === 'object' && tag !== null && Array.isArray(tag.values)) { + return tag.values.flatMap((value: any) => { + if (typeof value !== 'string') return [] + if (value.startsWith('#')) return allIngredientChoices({ tag: value.slice(1) }, itemTags) + try { + return [new ItemStack(Identifier.parse(value), 1)] + } catch (e) {} + return [] + }) + } + } + } + return [] +} diff --git a/src/app/components/previews/index.ts b/src/app/components/previews/index.ts index e33b5cd4..abadf129 100644 --- a/src/app/components/previews/index.ts +++ b/src/app/components/previews/index.ts @@ -9,6 +9,7 @@ export * from './LootTablePreview.jsx' export * from './ModelPreview.jsx' export * from './NoisePreview.js' export * from './NoiseSettingsPreview.js' +export * from './RecipePreview.jsx' export * from './StructureSetPreview.jsx' export type PreviewProps = {