feat: Add recipes, usages & guides for every item in the game to builtin JEI!

This commit is contained in:
Vitaly Turovsky 2024-06-19 00:51:00 +03:00
commit f8fcee780b
5 changed files with 1473 additions and 21 deletions

View file

@ -17,6 +17,7 @@ You can try this out at [mcraft.fun](https://mcraft.fun/), [pcm.gg](https://pcm.
- Play with friends over internet! (P2P is powered by Peer.js discovery servers)
- First-class touch (mobile) & controller support
- Resource pack support
- Builtin JEI with recipes & guides for every item (also replaces creative inventory)
- even even more!
All components that are in [Storybook](https://mcraft.fun/storybook) are published as npm module and can be used in other projects: [`minecraft-react`](https://npmjs.com/minecraft-react)

8
pnpm-lock.yaml generated
View file

@ -305,7 +305,7 @@ importers:
version: 1.0.0
minecraft-inventory-gui:
specifier: github:zardoy/minecraft-inventory-gui#next
version: https://codeload.github.com/zardoy/minecraft-inventory-gui/tar.gz/b424a566723067d0fb1a4bd70c1fb58a922f2ba4(@types/react@18.2.20)(react@18.2.0)
version: https://codeload.github.com/zardoy/minecraft-inventory-gui/tar.gz/5311971e783cc75e7759ea641bd1a15155d9c5cd(@types/react@18.2.20)(react@18.2.0)
mineflayer:
specifier: github:zardoy/mineflayer
version: https://codeload.github.com/zardoy/mineflayer/tar.gz/a4b1b4ba7f8c972cee9c0a16eb1191ff4d21fe23(encoding@0.1.13)
@ -6062,8 +6062,8 @@ packages:
minecraft-folder-path@1.2.0:
resolution: {integrity: sha512-qaUSbKWoOsH9brn0JQuBhxNAzTDMwrOXorwuRxdJKKKDYvZhtml+6GVCUrY5HRiEsieBEjCUnhVpDuQiKsiFaw==}
minecraft-inventory-gui@https://codeload.github.com/zardoy/minecraft-inventory-gui/tar.gz/b424a566723067d0fb1a4bd70c1fb58a922f2ba4:
resolution: {tarball: https://codeload.github.com/zardoy/minecraft-inventory-gui/tar.gz/b424a566723067d0fb1a4bd70c1fb58a922f2ba4}
minecraft-inventory-gui@https://codeload.github.com/zardoy/minecraft-inventory-gui/tar.gz/5311971e783cc75e7759ea641bd1a15155d9c5cd:
resolution: {tarball: https://codeload.github.com/zardoy/minecraft-inventory-gui/tar.gz/5311971e783cc75e7759ea641bd1a15155d9c5cd}
version: 1.0.1
minecraft-protocol@https://codeload.github.com/PrismarineJS/node-minecraft-protocol/tar.gz/495eed56ab230b2615596590064671356d86a2dc:
@ -15731,7 +15731,7 @@ snapshots:
minecraft-folder-path@1.2.0: {}
minecraft-inventory-gui@https://codeload.github.com/zardoy/minecraft-inventory-gui/tar.gz/b424a566723067d0fb1a4bd70c1fb58a922f2ba4(@types/react@18.2.20)(react@18.2.0):
minecraft-inventory-gui@https://codeload.github.com/zardoy/minecraft-inventory-gui/tar.gz/5311971e783cc75e7759ea641bd1a15155d9c5cd(@types/react@18.2.20)(react@18.2.0):
dependencies:
valtio: 1.11.2(@types/react@18.2.20)(react@18.2.0)
transitivePeerDependencies:

View file

@ -0,0 +1,41 @@
//@ts-check
// tsx ./scripts/getMissingRecipes.mjs
import MinecraftData from 'minecraft-data'
import supportedVersions from '../src/supportedVersions.mjs'
import fs from 'fs'
console.time('import-data')
const { descriptionGenerators } = await import('../src/itemsDescriptions')
console.timeEnd('import-data')
const data = MinecraftData(supportedVersions.at(-1))
const hasDescription = name => {
for (const [key, value] of descriptionGenerators) {
if (Array.isArray(key) && key.includes(name)) {
return true
}
if (key instanceof RegExp && key.test(name)) {
return true
}
}
return false
}
const result = []
for (const item of data.itemsArray) {
const recipes = data.recipes[item.id]
if (!recipes) {
if (item.name.endsWith('_slab') || item.name.endsWith('_stairs') || item.name.endsWith('_wall')) {
console.warn('Must have recipe!', item.name)
continue
}
if (hasDescription(item.name)) {
continue
}
result.push(item.name)
}
}
fs.writeFileSync('./generated/noRecipies.json', JSON.stringify(result, null, 2))

View file

@ -28,12 +28,13 @@ import nbt from 'prismarine-nbt'
import { splitEvery, equals } from 'rambda'
import PItem, { Item } from 'prismarine-item'
import Generic95 from '../assets/generic_95.png'
import { activeModalStack, hideCurrentModal, miscUiState, showModal } from './globalState'
import { activeModalStack, hideCurrentModal, hideModal, miscUiState, showModal } from './globalState'
import invspriteJson from './invsprite.json'
import { options } from './optionsStorage'
import { assertDefined, inGameError } from './utils'
import { MessageFormatPart } from './botUtils'
import { currentScaling } from './scaleInterface'
import { descriptionGenerators, getItemDescription } from './itemsDescriptions'
export const itemsAtlases: ItemsAtlasesOutputJson = _itemsAtlases
const loadedImagesCache = new Map<string, HTMLImageElement>()
@ -119,7 +120,9 @@ export const onGameLoad = (onLoad) => {
bot.on('windowClose', () => {
// todo hide up to the window itself!
hideCurrentModal()
if (lastWindow) {
hideCurrentModal()
}
})
bot.on('respawn', () => { // todo validate logic against native client (maybe login)
if (lastWindow) {
@ -417,6 +420,30 @@ const upJei = (search: string) => {
export const openItemsCanvas = (type, _bot = bot as typeof bot | null) => {
const inv = showInventory(type, getImage, {}, _bot)
inv.canvasManager.children[0].callbacks.getItemRecipes = (item) => {
const allRecipes = getAllItemRecipes(item.name)
inv.canvasManager.children[0].messageDisplay = ''
const itemDescription = getItemDescription(item)
if (!allRecipes?.length && !itemDescription) {
inv.canvasManager.children[0].messageDisplay = `No recipes found for ${item.displayName}`
}
return [...allRecipes ?? [], ...itemDescription ? [
[
'GenericDescription',
mapSlots([item])[0],
[],
itemDescription
]
] : []]
}
inv.canvasManager.children[0].callbacks.getItemUsages = (item) => {
const allItemUsages = getAllItemUsages(item.name)
inv.canvasManager.children[0].messageDisplay = ''
if (!allItemUsages?.length) {
inv.canvasManager.children[0].messageDisplay = `No usages found for ${item.displayName}`
}
return allItemUsages
}
return inv
}
@ -440,6 +467,7 @@ const openWindow = (type: string | undefined) => {
if (type !== undefined && bot.currentWindow && !skipClosePacketSending) bot.currentWindow['close']()
lastWindow.destroy()
lastWindow = null as any
window.lastWindow = lastWindow
miscUiState.displaySearchInput = false
destroyFn()
skipClosePacketSending = false
@ -452,8 +480,13 @@ const openWindow = (type: string | undefined) => {
inv.canvas.style.position = 'fixed'
inv.canvas.style.inset = '0'
inv.canvasManager.onClose = () => {
hideCurrentModal()
inv.canvasManager.onClose = async () => {
await new Promise(resolve => {
setTimeout(resolve, 0)
})
if (activeModalStack.at(-1)?.reactType?.includes('player_win:')) {
hideModal(undefined, undefined, { force: true })
}
inv.canvasManager.destroy()
}
@ -464,6 +497,18 @@ const openWindow = (type: string | undefined) => {
upWindowItems()
lastWindow.pwindow.touch = miscUiState.currentTouch
const oldOnInventoryEvent = lastWindow.pwindow.onInventoryEvent.bind(lastWindow.pwindow)
lastWindow.pwindow.onInventoryEvent = (type, containing, windowIndex, inventoryIndex, item) => {
if (inv.canvasManager.children[0].currentGuide) {
const isRightClick = type === 'rightclick'
const isLeftClick = type === 'leftclick'
if (isLeftClick || isRightClick) {
inv.canvasManager.children[0].showRecipesOrUsages(isLeftClick, item)
}
} else {
oldOnInventoryEvent(type, containing, windowIndex, inventoryIndex, item)
}
}
lastWindow.pwindow.onJeiClick = (slotItem, _index, isRightclick) => {
// slotItem is the slot from mapSlots
const itemId = loadedData.itemsByName[slotItem.name]?.id
@ -472,21 +517,25 @@ const openWindow = (type: string | undefined) => {
return
}
const item = new PrismarineItem(itemId, isRightclick ? 64 : 1, slotItem.metadata)
const freeSlot = bot.inventory.firstEmptyInventorySlot()
if (freeSlot === null) return
void bot.creative.setInventorySlot(freeSlot, item)
if (bot.game.gameMode === 'creative') {
const freeSlot = bot.inventory.firstEmptyInventorySlot()
if (freeSlot === null) return
void bot.creative.setInventorySlot(freeSlot, item)
} else {
inv.canvasManager.children[0].showRecipesOrUsages(!isRightclick, mapSlots([item])[0])
}
}
if (bot.game.gameMode === 'creative') {
lastWindow.pwindow.win.jeiSlotsPage = 0
// todo workaround so inventory opens immediately (but still lags)
setTimeout(() => {
upJei('')
})
miscUiState.displaySearchInput = true
} else {
lastWindow.pwindow.win.jeiSlots = []
}
// if (bot.game.gameMode !== 'spectator') {
lastWindow.pwindow.win.jeiSlotsPage = 0
// todo workaround so inventory opens immediately (though it still lags)
setTimeout(() => {
upJei('')
})
miscUiState.displaySearchInput = true
// } else {
// lastWindow.pwindow.win.jeiSlots = []
// }
if (type === undefined) {
// player inventory
@ -553,3 +602,74 @@ const getResultingRecipe = (slots: Array<Item | null>, gridRows: number) => {
const item = new PrismarineItem(id, count, metadata)
return item
}
const getAllItemRecipes = (itemName: string) => {
const item = loadedData.itemsByName[itemName]
if (!item) return
const itemId = item.id
const recipes = loadedData.recipes[itemId]
if (!recipes) return
const results = [] as Array<{
result: Item,
ingredients: Item[],
description?: string
}>
// get recipes here
for (const recipe of recipes) {
const { result } = recipe
if (!result) continue
const resultId = typeof result === 'number' ? result : Array.isArray(result) ? result[0]! : result.id
const resultCount = (typeof result === 'number' ? undefined : Array.isArray(result) ? result[1] : result.count) ?? 1
const resultMetadata = typeof result === 'object' && !Array.isArray(result) ? result.metadata : undefined
const resultItem = new PrismarineItem(resultId!, resultCount, resultMetadata)
if ('inShape' in recipe) {
const ingredients = recipe.inShape
if (!ingredients) continue
// eslint-disable-next-line @typescript-eslint/no-loop-func
const ingredientsItems = ingredients.flatMap(items => items.map(item => new PrismarineItem((item ?? 0) as number, 1)))
results.push({ result: resultItem, ingredients: ingredientsItems })
}
if ('ingredients' in recipe) {
const { ingredients } = recipe
if (!ingredients) continue
// eslint-disable-next-line @typescript-eslint/no-loop-func
const ingredientsItems = ingredients.map(item => new PrismarineItem((item ?? 0) as number, 1))
results.push({ result: resultItem, ingredients: ingredientsItems, description: 'Shapeless' })
}
}
return results.map(({ result, ingredients, description }) => {
return [
'CraftingTableGuide',
mapSlots([result])[0],
mapSlots(ingredients),
description
]
})
}
const getAllItemUsages = (itemName: string) => {
const item = loadedData.itemsByName[itemName]
if (!item) return
const foundRecipeIds = [] as string[]
for (const [id, recipes] of Object.entries(loadedData.recipes)) {
for (const recipe of recipes) {
if ('inShape' in recipe) {
if (recipe.inShape.some(row => row.includes(item.id))) {
foundRecipeIds.push(id)
}
}
if ('ingredients' in recipe) {
if (recipe.ingredients.includes(item.id)) {
foundRecipeIds.push(id)
}
}
}
}
return foundRecipeIds.flatMap(id => {
// todo should use exact match, not include all recipes!
return getAllItemRecipes(loadedData.items[id].name)
})
}

1290
src/itemsDescriptions.ts Normal file

File diff suppressed because it is too large Load diff