minecraft-inventory 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (51) hide show
  1. package/README.md +566 -0
  2. package/package.json +37 -0
  3. package/src/InventoryGUI.tsx +108 -0
  4. package/src/cache/textureCache.ts +95 -0
  5. package/src/components/CursorItem/CursorItem.tsx +94 -0
  6. package/src/components/CursorItem/index.ts +1 -0
  7. package/src/components/HUD/HUD.tsx +11 -0
  8. package/src/components/HUD/index.ts +1 -0
  9. package/src/components/Hotbar/Hotbar.tsx +180 -0
  10. package/src/components/Hotbar/index.ts +1 -0
  11. package/src/components/InventoryOverlay/InventoryOverlay.tsx +196 -0
  12. package/src/components/InventoryOverlay/index.ts +2 -0
  13. package/src/components/InventoryWindow/EnchantmentOptions.tsx +109 -0
  14. package/src/components/InventoryWindow/InventoryBackground.tsx +110 -0
  15. package/src/components/InventoryWindow/InventoryWindow.tsx +120 -0
  16. package/src/components/InventoryWindow/ProgressBar.tsx +78 -0
  17. package/src/components/InventoryWindow/VillagerTradeList.tsx +136 -0
  18. package/src/components/InventoryWindow/index.ts +5 -0
  19. package/src/components/ItemCanvas/ItemCanvas.tsx +154 -0
  20. package/src/components/ItemCanvas/index.ts +1 -0
  21. package/src/components/JEI/JEI.module.css +37 -0
  22. package/src/components/JEI/JEI.tsx +303 -0
  23. package/src/components/JEI/index.ts +2 -0
  24. package/src/components/RecipeGuide/RecipeInventoryView.tsx +293 -0
  25. package/src/components/RecipeGuide/index.ts +1 -0
  26. package/src/components/Slot/Slot.module.css +111 -0
  27. package/src/components/Slot/Slot.tsx +363 -0
  28. package/src/components/Slot/index.ts +1 -0
  29. package/src/components/Text/MessageFormatted.css +5 -0
  30. package/src/components/Text/MessageFormatted.tsx +79 -0
  31. package/src/components/Text/MessageFormattedString.tsx +74 -0
  32. package/src/components/Text/chatUtils.ts +172 -0
  33. package/src/components/Tooltip/Tooltip.module.css +56 -0
  34. package/src/components/Tooltip/Tooltip.tsx +130 -0
  35. package/src/components/Tooltip/index.ts +1 -0
  36. package/src/connector/demo.ts +213 -0
  37. package/src/connector/index.ts +4 -0
  38. package/src/connector/mineflayer.ts +113 -0
  39. package/src/connector/types.ts +41 -0
  40. package/src/context/InventoryContext.tsx +157 -0
  41. package/src/context/ScaleContext.tsx +73 -0
  42. package/src/context/TextureContext.tsx +70 -0
  43. package/src/globals.d.ts +4 -0
  44. package/src/hooks/useKeyboardShortcuts.ts +41 -0
  45. package/src/hooks/useMobile.ts +28 -0
  46. package/src/index.tsx +65 -0
  47. package/src/mount.tsx +52 -0
  48. package/src/registry/index.ts +21 -0
  49. package/src/registry/inventories.ts +612 -0
  50. package/src/styles/tokens.css +47 -0
  51. package/src/types.ts +176 -0
@@ -0,0 +1,157 @@
1
+ import React, { createContext, useContext, useEffect, useRef, useState, useCallback } from 'react'
2
+ import type { InventoryWindowState, PlayerState, ItemStack, SlotState } from '../types'
3
+ import type { InventoryConnector } from '../connector/types'
4
+
5
+ export interface InventoryContextValue {
6
+ windowState: InventoryWindowState | null
7
+ playerState: PlayerState | null
8
+ heldItem: ItemStack | null
9
+ setHeldItem: (item: ItemStack | null) => void
10
+ connector: InventoryConnector | null
11
+ sendAction: InventoryConnector['sendAction']
12
+ isDragging: boolean
13
+ dragSlots: number[]
14
+ dragButton: 'left' | 'right' | null
15
+ startDrag: (slotIndex: number, button: 'left' | 'right') => void
16
+ addDragSlot: (slotIndex: number) => void
17
+ endDrag: () => void
18
+ cancelDrag: () => void
19
+ hoveredSlot: number | null
20
+ setHoveredSlot: (slot: number | null) => void
21
+ activeNumberKey: number | null
22
+ setActiveNumberKey: (key: number | null) => void
23
+ getSlot: (index: number) => SlotState | undefined
24
+ /** Pixel offset from the cursor item's top-left to the grab point, preserving pick-up position */
25
+ grabOffset: { x: number; y: number }
26
+ setGrabOffset: (offset: { x: number; y: number }) => void
27
+ }
28
+
29
+ const InventoryContext = createContext<InventoryContextValue | null>(null)
30
+
31
+ export function useInventoryContext(): InventoryContextValue {
32
+ const ctx = useContext(InventoryContext)
33
+ if (!ctx) throw new Error('useInventoryContext must be used within InventoryProvider')
34
+ return ctx
35
+ }
36
+
37
+ interface InventoryProviderProps {
38
+ connector: InventoryConnector | null
39
+ children: React.ReactNode
40
+ }
41
+
42
+ export function InventoryProvider({ connector, children }: InventoryProviderProps) {
43
+ const [windowState, setWindowState] = useState<InventoryWindowState | null>(
44
+ () => connector?.getWindowState() ?? null,
45
+ )
46
+ const [playerState, setPlayerState] = useState<PlayerState | null>(
47
+ () => connector?.getPlayerState() ?? null,
48
+ )
49
+ const [heldItem, setHeldItemState] = useState<ItemStack | null>(
50
+ () => connector?.getWindowState()?.heldItem ?? null,
51
+ )
52
+ const [hoveredSlot, setHoveredSlot] = useState<number | null>(null)
53
+ const [isDragging, setIsDragging] = useState(false)
54
+ const [dragSlots, setDragSlots] = useState<number[]>([])
55
+ const [dragButton, setDragButton] = useState<'left' | 'right' | null>(null)
56
+ const [activeNumberKey, setActiveNumberKey] = useState<number | null>(null)
57
+ const [grabOffset, setGrabOffset] = useState<{ x: number; y: number }>({ x: 0, y: 0 })
58
+
59
+ const connectorRef = useRef(connector)
60
+ connectorRef.current = connector
61
+
62
+ useEffect(() => {
63
+ if (!connector) return
64
+ setWindowState(connector.getWindowState())
65
+ setPlayerState(connector.getPlayerState())
66
+
67
+ return connector.subscribe((event) => {
68
+ if (event.type === 'windowOpen' || event.type === 'windowUpdate') {
69
+ setWindowState({ ...event.state })
70
+ setHeldItemState(event.state.heldItem)
71
+ } else if (event.type === 'windowClose') {
72
+ setWindowState(null)
73
+ setHeldItemState(null)
74
+ setIsDragging(false)
75
+ setDragSlots([])
76
+ } else if (event.type === 'playerUpdate') {
77
+ setPlayerState(event.state)
78
+ } else if (event.type === 'heldItemChange') {
79
+ setHeldItemState(event.item)
80
+ }
81
+ })
82
+ }, [connector])
83
+
84
+ const setHeldItem = useCallback((item: ItemStack | null) => {
85
+ setHeldItemState(item)
86
+ }, [])
87
+
88
+ const sendAction = useCallback<InventoryConnector['sendAction']>(
89
+ (action) => {
90
+ return connectorRef.current?.sendAction(action)
91
+ },
92
+ [],
93
+ )
94
+
95
+ const startDrag = useCallback((slotIndex: number, button: 'left' | 'right') => {
96
+ setIsDragging(true)
97
+ setDragButton(button)
98
+ setDragSlots([slotIndex])
99
+ connectorRef.current?.sendAction({ type: 'drag', slots: [], button })
100
+ }, [])
101
+
102
+ const addDragSlot = useCallback((slotIndex: number) => {
103
+ setDragSlots((prev) => {
104
+ if (prev.includes(slotIndex)) return prev
105
+ return [...prev, slotIndex]
106
+ })
107
+ }, [])
108
+
109
+ const endDrag = useCallback(() => {
110
+ setDragSlots((slots) => {
111
+ if (slots.length > 0 && dragButton) {
112
+ connectorRef.current?.sendAction({ type: 'drag', slots, button: dragButton })
113
+ }
114
+ return []
115
+ })
116
+ setIsDragging(false)
117
+ setDragButton(null)
118
+ }, [dragButton])
119
+
120
+ const cancelDrag = useCallback(() => {
121
+ setIsDragging(false)
122
+ setDragSlots([])
123
+ setDragButton(null)
124
+ }, [])
125
+
126
+ const getSlot = useCallback(
127
+ (index: number) => {
128
+ return windowState?.slots.find((s) => s.index === index)
129
+ },
130
+ [windowState],
131
+ )
132
+
133
+ const value: InventoryContextValue = {
134
+ windowState,
135
+ playerState,
136
+ heldItem,
137
+ setHeldItem,
138
+ connector,
139
+ sendAction,
140
+ isDragging,
141
+ dragSlots,
142
+ dragButton,
143
+ startDrag,
144
+ addDragSlot,
145
+ endDrag,
146
+ cancelDrag,
147
+ hoveredSlot,
148
+ setHoveredSlot,
149
+ activeNumberKey,
150
+ setActiveNumberKey,
151
+ getSlot,
152
+ grabOffset,
153
+ setGrabOffset,
154
+ }
155
+
156
+ return <InventoryContext.Provider value={value}>{children}</InventoryContext.Provider>
157
+ }
@@ -0,0 +1,73 @@
1
+ import React, { createContext, useContext, useMemo } from 'react'
2
+
3
+ export interface ScaleContextValue {
4
+ scale: number
5
+ /** Full grid cell size: 18 × scale. Used for layout/positioning. */
6
+ slotSize: number
7
+ /** Item content area size: slotSize - 2 × borderPx. This is the slot div size. */
8
+ contentSize: number
9
+ /** 1 unscaled pixel, Math.max(1, Math.round(scale)). Offset from grid cell to content area. */
10
+ borderPx: number
11
+ fontSize: number
12
+ borderRadius: number
13
+ pixelSize: number
14
+ getCSSVars(): React.CSSProperties
15
+ }
16
+
17
+ const BASE_SLOT_SIZE = 18
18
+
19
+ const ScaleContext = createContext<ScaleContextValue>({
20
+ scale: 2,
21
+ slotSize: BASE_SLOT_SIZE * 2,
22
+ contentSize: BASE_SLOT_SIZE * 2 - 4,
23
+ borderPx: 2,
24
+ fontSize: 12,
25
+ borderRadius: 0,
26
+ pixelSize: 2,
27
+ getCSSVars: () => ({}),
28
+ })
29
+
30
+ export function useScale(): ScaleContextValue {
31
+ return useContext(ScaleContext)
32
+ }
33
+
34
+ interface ScaleProviderProps {
35
+ scale?: number
36
+ children: React.ReactNode
37
+ }
38
+
39
+ export function ScaleProvider({ scale = 2, children }: ScaleProviderProps) {
40
+ const value = useMemo<ScaleContextValue>(() => {
41
+ const slotSize = BASE_SLOT_SIZE * scale
42
+ const fontSize = 7 * scale
43
+ const pixelSize = scale
44
+ const borderPx = Math.max(1, Math.round(scale))
45
+ const contentSize = slotSize - 2 * borderPx
46
+
47
+ return {
48
+ scale,
49
+ slotSize,
50
+ contentSize,
51
+ borderPx,
52
+ fontSize,
53
+ borderRadius: 0,
54
+ pixelSize,
55
+ getCSSVars: () => ({
56
+ '--mc-scale': scale,
57
+ '--mc-slot-size': `${slotSize}px`,
58
+ '--mc-font-size': `${fontSize}px`,
59
+ '--mc-pixel': `${pixelSize}px`,
60
+ '--mc-border': `${borderPx}px`,
61
+ '--mc-gap': `${2 * pixelSize}px`,
62
+ '--mc-padding': `${4 * pixelSize}px`,
63
+ '--mc-count-font': `${Math.round(6 * scale)}px`,
64
+ } as React.CSSProperties),
65
+ }
66
+ }, [scale])
67
+
68
+ return (
69
+ <ScaleContext.Provider value={value}>
70
+ <div style={value.getCSSVars()}>{children}</div>
71
+ </ScaleContext.Provider>
72
+ )
73
+ }
@@ -0,0 +1,70 @@
1
+ import React, { createContext, useContext } from 'react'
2
+
3
+ // Default: 1.16.4 for GUI textures (most containers existed here; 1.21.4 for items)
4
+ export const MC_ITEMS_BASE =
5
+ 'https://raw.githubusercontent.com/PrismarineJS/minecraft-assets/master/data/1.21.4'
6
+ export const MC_GUI_BASE =
7
+ 'https://raw.githubusercontent.com/PrismarineJS/minecraft-assets/master/data'
8
+
9
+ export interface TextureConfig {
10
+ baseUrl: string
11
+ getItemTextureUrl(item: { type: number; name?: string }): string
12
+ getBlockTextureUrl(item: { type: number; name?: string }): string
13
+ /** version defaults to '1.16.4' for GUI textures unless overridden per-container */
14
+ getGuiTextureUrl(path: string, version?: string): string
15
+ }
16
+
17
+ function buildDefault(base: string): TextureConfig {
18
+ const isRemote = base.startsWith('http')
19
+ // For remote default: use the same base for items, separate versioned base for GUI
20
+ const guiBase = isRemote ? MC_GUI_BASE : base
21
+
22
+ return {
23
+ baseUrl: base,
24
+
25
+ getItemTextureUrl({ type, name }) {
26
+ const root = isRemote ? MC_ITEMS_BASE : base
27
+ if (name) return `${root}/items/${name}.png`
28
+ return `${root}/items/${type}.png`
29
+ },
30
+
31
+ getBlockTextureUrl({ type, name }) {
32
+ const root = isRemote ? MC_ITEMS_BASE : base
33
+ if (name) return `${root}/blocks/${name}.png`
34
+ return `${root}/blocks/${type}.png`
35
+ },
36
+
37
+ getGuiTextureUrl(path: string, version = '1.16.4') {
38
+ if (isRemote) {
39
+ return `${guiBase}/${version}/${path}.png`
40
+ }
41
+ return `${base}/textures/${path}.png`
42
+ },
43
+ }
44
+ }
45
+
46
+ const defaultTextureConfig = buildDefault(MC_ITEMS_BASE)
47
+
48
+ const TextureContext = createContext<TextureConfig>(defaultTextureConfig)
49
+
50
+ export function useTextures(): TextureConfig {
51
+ return useContext(TextureContext)
52
+ }
53
+
54
+ interface TextureProviderProps {
55
+ config?: Partial<TextureConfig>
56
+ baseUrl?: string
57
+ children: React.ReactNode
58
+ }
59
+
60
+ export function TextureProvider({ config, baseUrl, children }: TextureProviderProps) {
61
+ const base = baseUrl ?? config?.baseUrl ?? MC_ITEMS_BASE
62
+ const merged: TextureConfig = {
63
+ ...buildDefault(base),
64
+ ...config,
65
+ }
66
+
67
+ return <TextureContext.Provider value={merged}>{children}</TextureContext.Provider>
68
+ }
69
+
70
+ export { defaultTextureConfig }
@@ -0,0 +1,4 @@
1
+ declare module '*.module.css' {
2
+ const content: Record<string, string>
3
+ export default content
4
+ }
@@ -0,0 +1,41 @@
1
+ import { useEffect } from 'react'
2
+ import { useInventoryContext } from '../context/InventoryContext'
3
+
4
+ export function useKeyboardShortcuts(enabled = true) {
5
+ const { setActiveNumberKey, hoveredSlot, sendAction } = useInventoryContext()
6
+
7
+ useEffect(() => {
8
+ if (!enabled) return
9
+
10
+ const handleKeyDown = (e: KeyboardEvent) => {
11
+ // Digit1–Digit9 → hotbar swap (works regardless of keyboard layout)
12
+ if (e.code >= 'Digit1' && e.code <= 'Digit9') {
13
+ setActiveNumberKey(parseInt(e.code.replace('Digit', ''), 10) - 1)
14
+ }
15
+
16
+ // Q to drop
17
+ if (e.code === 'KeyQ') {
18
+ if (hoveredSlot !== null) {
19
+ sendAction({
20
+ type: 'drop',
21
+ slotIndex: hoveredSlot,
22
+ all: e.ctrlKey || e.metaKey,
23
+ })
24
+ }
25
+ }
26
+ }
27
+
28
+ const handleKeyUp = (e: KeyboardEvent) => {
29
+ if (e.code >= 'Digit1' && e.code <= 'Digit9') {
30
+ setActiveNumberKey(null)
31
+ }
32
+ }
33
+
34
+ window.addEventListener('keydown', handleKeyDown)
35
+ window.addEventListener('keyup', handleKeyUp)
36
+ return () => {
37
+ window.removeEventListener('keydown', handleKeyDown)
38
+ window.removeEventListener('keyup', handleKeyUp)
39
+ }
40
+ }, [enabled, hoveredSlot, sendAction, setActiveNumberKey])
41
+ }
@@ -0,0 +1,28 @@
1
+ import { useState, useEffect } from 'react'
2
+
3
+ let cachedIsMobile: boolean | null = null
4
+
5
+ function detectMobile(): boolean {
6
+ if (typeof window === 'undefined') return false
7
+ return (
8
+ 'ontouchstart' in window ||
9
+ navigator.maxTouchPoints > 0 ||
10
+ window.matchMedia('(pointer: coarse)').matches
11
+ )
12
+ }
13
+
14
+ export function useMobile(): boolean {
15
+ const [isMobile, setIsMobile] = useState<boolean>(() => {
16
+ if (cachedIsMobile !== null) return cachedIsMobile
17
+ return detectMobile()
18
+ })
19
+
20
+ useEffect(() => {
21
+ const mq = window.matchMedia('(pointer: coarse)')
22
+ const update = (e: MediaQueryListEvent) => setIsMobile(e.matches)
23
+ mq.addEventListener('change', update)
24
+ return () => mq.removeEventListener('change', update)
25
+ }, [])
26
+
27
+ return isMobile
28
+ }
package/src/index.tsx ADDED
@@ -0,0 +1,65 @@
1
+ export { InventoryGUI } from './InventoryGUI'
2
+ export type { InventoryGUIProps } from './InventoryGUI'
3
+
4
+ // Contexts & Providers
5
+ export { InventoryProvider, useInventoryContext } from './context/InventoryContext'
6
+ export { ScaleProvider, useScale } from './context/ScaleContext'
7
+ export { TextureProvider, useTextures, defaultTextureConfig } from './context/TextureContext'
8
+
9
+ // Components
10
+ export { InventoryWindow } from './components/InventoryWindow'
11
+ export { InventoryBackground } from './components/InventoryWindow'
12
+ export { Slot } from './components/Slot'
13
+ export { ItemCanvas } from './components/ItemCanvas'
14
+ export { Tooltip } from './components/Tooltip'
15
+ export { JEI } from './components/JEI'
16
+ export type { JEIItem } from './components/JEI'
17
+ export { Hotbar } from './components/Hotbar'
18
+ export { HUD } from './components/HUD'
19
+ export { CursorItem } from './components/CursorItem'
20
+ export { InventoryOverlay } from './components/InventoryOverlay'
21
+ export type { InventoryOverlayProps } from './components/InventoryOverlay'
22
+ export { RecipeInventoryView } from './components/RecipeGuide'
23
+
24
+ // Text components
25
+ export { MessageFormattedString } from './components/Text/MessageFormattedString'
26
+ export { default as MessageFormatted, MessagePart } from './components/Text/MessageFormatted'
27
+
28
+ // Programmatic mount (no React needed)
29
+ export { mountInventory } from './mount'
30
+ export type { MountedInventory } from './mount'
31
+
32
+ // Hooks
33
+ export { useMobile } from './hooks/useMobile'
34
+ export { useKeyboardShortcuts } from './hooks/useKeyboardShortcuts'
35
+
36
+ // Connector
37
+ export { createMineflayerConnector } from './connector/mineflayer'
38
+ export { createDemoConnector } from './connector/demo'
39
+ export type {
40
+ InventoryConnector,
41
+ ConnectorEvent,
42
+ ConnectorListener,
43
+ MineflayerBot,
44
+ } from './connector/types'
45
+ export type { ActionLogEntry, DemoConnectorOptions } from './connector/demo'
46
+
47
+ // Registry
48
+ export { registerInventoryType, getInventoryType, getAllInventoryTypes } from './registry'
49
+ export type { InventoryTypeDefinition } from './registry'
50
+
51
+ // Types
52
+ export type {
53
+ ItemStack,
54
+ SlotState,
55
+ SlotDefinition,
56
+ ProgressBar,
57
+ InventoryWindowState,
58
+ PlayerState,
59
+ TradeOffer,
60
+ InventoryAction,
61
+ MouseButton,
62
+ ClickMode,
63
+ RecipeGuide,
64
+ RecipeNavFrame,
65
+ } from './types'
package/src/mount.tsx ADDED
@@ -0,0 +1,52 @@
1
+ import React from 'react'
2
+ import { createRoot, type Root } from 'react-dom/client'
3
+ import { InventoryGUI, type InventoryGUIProps } from './InventoryGUI'
4
+
5
+ export interface MountedInventory {
6
+ update(props: Partial<InventoryGUIProps>): void
7
+ destroy(): void
8
+ }
9
+
10
+ /**
11
+ * Mount an inventory GUI into a DOM element without writing React code.
12
+ *
13
+ * ```js
14
+ * import { mountInventory, createDemoConnector } from 'minecraft-inventory'
15
+ *
16
+ * const connector = createDemoConnector({ windowType: 'chest', slots: [] })
17
+ * const inv = mountInventory(document.getElementById('root'), {
18
+ * type: 'chest',
19
+ * connector,
20
+ * scale: 2,
21
+ * })
22
+ *
23
+ * // Later: update props
24
+ * inv.update({ scale: 3 })
25
+ *
26
+ * // Cleanup
27
+ * inv.destroy()
28
+ * ```
29
+ */
30
+ export function mountInventory(
31
+ container: HTMLElement,
32
+ initialProps: InventoryGUIProps,
33
+ ): MountedInventory {
34
+ let currentProps = { ...initialProps }
35
+ const root: Root = createRoot(container)
36
+
37
+ function render() {
38
+ root.render(<InventoryGUI {...currentProps} />)
39
+ }
40
+
41
+ render()
42
+
43
+ return {
44
+ update(props: Partial<InventoryGUIProps>) {
45
+ currentProps = { ...currentProps, ...props }
46
+ render()
47
+ },
48
+ destroy() {
49
+ root.unmount()
50
+ },
51
+ }
52
+ }
@@ -0,0 +1,21 @@
1
+ import type { InventoryTypeDefinition } from '../types'
2
+ import { inventoryDefinitions } from './inventories'
3
+
4
+ const registry = new Map<string, InventoryTypeDefinition>(
5
+ Object.entries(inventoryDefinitions),
6
+ )
7
+
8
+ export function registerInventoryType(def: InventoryTypeDefinition): void {
9
+ registry.set(def.name, def)
10
+ }
11
+
12
+ export function getInventoryType(name: string): InventoryTypeDefinition | undefined {
13
+ return registry.get(name)
14
+ }
15
+
16
+ export function getAllInventoryTypes(): InventoryTypeDefinition[] {
17
+ return Array.from(registry.values())
18
+ }
19
+
20
+ export { inventoryDefinitions }
21
+ export type { InventoryTypeDefinition }