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 [] }