minecraft-inventory 0.1.4 → 0.1.5

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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "minecraft-inventory",
3
- "version": "0.1.4",
3
+ "version": "0.1.5",
4
4
  "type": "module",
5
5
  "license": "MIT",
6
6
  "release": {
@@ -1,54 +1,24 @@
1
- import React, { useEffect, useState } from 'react'
1
+ import React from 'react'
2
2
  import { useTextures } from '../../context/TextureContext'
3
3
  import { useScale } from '../../context/ScaleContext'
4
4
  import { MessageFormattedString } from '../Text/MessageFormattedString'
5
5
  import type { InventoryTypeDefinition } from '../../registry'
6
6
 
7
7
  /**
8
- * For generic_9xN (N < 6): canvas-stitch the 6-row generic_54 texture.
9
- * Takes the top (title + N rows) and bottom (player-inventory section = last 96px)
10
- * from the source and composes them into a data URL of the correct output height.
8
+ * For generic_9xN (N < 6): CSS-stitch the 6-row generic_54 texture using two
9
+ * overlapping img elements with objectPosition to clip top and bottom portions.
10
+ * No canvas or async loading needed works with any URL including remote ones.
11
11
  *
12
- * The source texture (generic_54.png, 176×222) layout:
13
- * y=0..16 — title bar (17px)
14
- * y=17..124 — 6 container rows (6×18 = 108px)
15
- * y=126..221 — player inventory section (96px)
12
+ * The source texture (generic_54.png, 176×222) fixed layout:
13
+ * y=0 ..16 — title bar (17px)
14
+ * y=17 ..124 — 6 container rows (6×18 = 108px)
15
+ * y=125 ..221 — player inventory section (97px)
16
+ *
17
+ * Output height = topH + playerH = N*18+17 + 97 = N*18+114.
18
+ * Registry backgroundHeight must equal N*18+114 for the div to match.
16
19
  */
17
- function useStitchedTexture(srcUrl: string, containerRows: number): string | null {
18
- const [dataUrl, setDataUrl] = useState<string | null>(null)
19
-
20
- useEffect(() => {
21
- if (containerRows >= 6) {
22
- setDataUrl(srcUrl)
23
- return
24
- }
25
- let cancelled = false
26
- const img = new window.Image()
27
- img.crossOrigin = 'anonymous'
28
- img.onload = () => {
29
- if (cancelled) return
30
- const srcW = 176
31
- const srcH = img.naturalHeight // 222 for generic_54
32
- const topH = containerRows * 18 + 17
33
- const playerH = 96
34
- const canvas = document.createElement('canvas')
35
- canvas.width = srcW
36
- canvas.height = topH + playerH
37
- const ctx = canvas.getContext('2d')
38
- if (!ctx) { setDataUrl(srcUrl); return }
39
- // Top: title + N container rows
40
- ctx.drawImage(img, 0, 0, srcW, topH, 0, 0, srcW, topH)
41
- // Bottom: player inventory section (last 96px of source)
42
- ctx.drawImage(img, 0, srcH - playerH, srcW, playerH, 0, topH, srcW, playerH)
43
- setDataUrl(canvas.toDataURL())
44
- }
45
- img.onerror = () => { if (!cancelled) setDataUrl(srcUrl) }
46
- img.src = srcUrl
47
- return () => { cancelled = true }
48
- }, [srcUrl, containerRows])
49
-
50
- return dataUrl
51
- }
20
+ const SRC_PLAYER_Y = 17 + 6 * 18 // 125 — fixed position where player section starts
21
+ const PLAYER_H = 222 - SRC_PLAYER_Y // 97px — height of player section in generic_54.png
52
22
 
53
23
  interface InventoryBackgroundProps {
54
24
  definition: InventoryTypeDefinition
@@ -66,20 +36,24 @@ export function InventoryBackground({
66
36
  const textures = useTextures()
67
37
  const { scale } = useScale()
68
38
 
69
- const rawBgUrl = textures.getGuiTextureUrl(definition.backgroundTexture)
70
- // For generic_9xN (containerRows defined and < 6): canvas-stitch the texture
71
- const stitchedUrl = useStitchedTexture(rawBgUrl, definition.containerRows ?? 6)
72
- const bgUrl = definition.containerRows != null && definition.containerRows < 6
73
- ? stitchedUrl // may be null while stitching
74
- : rawBgUrl
39
+ const bgUrl = textures.getGuiTextureUrl(definition.backgroundTexture)
40
+ const isStitched = definition.containerRows != null && definition.containerRows < 6
75
41
 
76
42
  const w = definition.backgroundWidth * scale
77
43
  const h = definition.backgroundHeight * scale
78
44
 
79
- // Source dimensions from definition (e.g., 176x166) — clip to this region from texture
80
45
  const srcW = definition.backgroundWidth
81
46
  const srcH = definition.backgroundHeight
82
47
 
48
+ const sharedImgStyle: React.CSSProperties = {
49
+ display: 'block',
50
+ width: srcW,
51
+ imageRendering: 'pixelated',
52
+ pointerEvents: 'none',
53
+ userSelect: 'none',
54
+ objectFit: 'none',
55
+ }
56
+
83
57
  return (
84
58
  <div
85
59
  className="mc-inv-background"
@@ -93,42 +67,55 @@ export function InventoryBackground({
93
67
  outlineOffset: 0,
94
68
  }}
95
69
  >
96
- {/* Background texture wrapper — clips source to srcW×srcH, then scales */}
97
- <div
98
- className="mc-inv-background-wrapper"
99
- style={{
100
- position: 'absolute',
101
- top: 0,
102
- left: 0,
103
- width: srcW,
104
- height: srcH,
105
- overflow: 'hidden',
106
- transform: `scale(${scale})`,
107
- transformOrigin: 'top left',
108
- }}
109
- >
110
- {/* Background texture — render at natural size, clipped by wrapper overflow */}
111
- {bgUrl && (
112
- <img
113
- className="mc-inv-background-image"
114
- src={bgUrl}
115
- alt=""
116
- aria-hidden
70
+ {isStitched ? (
71
+ /* CSS two-part stitch: clips top (title+N rows) and bottom (player section)
72
+ from the same source image using objectPosition — no canvas/async needed. */
73
+ <div
74
+ className="mc-inv-background-wrapper"
117
75
  style={{
118
- display: 'block',
76
+ position: 'absolute',
77
+ top: 0,
78
+ left: 0,
79
+ transform: `scale(${scale})`,
80
+ transformOrigin: 'top left',
81
+ }}
82
+ >
83
+ {/* Top: title bar + N container rows */}
84
+ <div style={{ width: srcW, height: definition.containerRows! * 18 + 17, overflow: 'hidden' }}>
85
+ <img className="mc-inv-background-image" src={bgUrl} alt="" aria-hidden draggable={false}
86
+ style={{ ...sharedImgStyle, objectPosition: '0 0' }} />
87
+ </div>
88
+ {/* Bottom: player inventory section starting at SRC_PLAYER_Y in source */}
89
+ <div style={{ width: srcW, height: PLAYER_H, overflow: 'hidden' }}>
90
+ <img src={bgUrl} alt="" aria-hidden draggable={false}
91
+ style={{ ...sharedImgStyle, objectPosition: `0 -${SRC_PLAYER_Y}px` }} />
92
+ </div>
93
+ </div>
94
+ ) : (
95
+ /* Standard: clip source to srcW×srcH via overflow:hidden, then scale */
96
+ <div
97
+ className="mc-inv-background-wrapper"
98
+ style={{
99
+ position: 'absolute',
100
+ top: 0,
101
+ left: 0,
119
102
  width: srcW,
120
103
  height: srcH,
121
- imageRendering: 'pixelated',
122
- pointerEvents: 'none',
123
- userSelect: 'none',
124
- // Clip to top-left srcW×srcH region (if texture is larger)
125
- objectFit: 'none',
126
- objectPosition: '0 0',
104
+ overflow: 'hidden',
105
+ transform: `scale(${scale})`,
106
+ transformOrigin: 'top left',
127
107
  }}
128
- draggable={false}
129
- />
130
- )}
131
- </div>
108
+ >
109
+ <img
110
+ className="mc-inv-background-image"
111
+ src={bgUrl}
112
+ alt=""
113
+ aria-hidden
114
+ draggable={false}
115
+ style={{ ...sharedImgStyle, objectPosition: '0 0' }}
116
+ />
117
+ </div>
118
+ )}
132
119
 
133
120
  {/* Title */}
134
121
  {title !== undefined && (
@@ -45,6 +45,7 @@ export function Notes({
45
45
  const [notes, setNotes] = useState<Note[]>([])
46
46
  const [draft, setDraft] = useState('')
47
47
  const [loading, setLoading] = useState(true)
48
+ const [showInput, setShowInput] = useState(false)
48
49
 
49
50
  // Load notes on mount
50
51
  useEffect(() => {
@@ -104,6 +105,14 @@ export function Notes({
104
105
  setDraft('')
105
106
  }, [draft, notes, saveNotes])
106
107
 
108
+ const handleAddButtonClick = useCallback(() => {
109
+ if (!showInput) {
110
+ setShowInput(true)
111
+ } else {
112
+ addNote()
113
+ }
114
+ }, [showInput, addNote])
115
+
107
116
  const removeNote = useCallback((id: number) => {
108
117
  const next = notes.filter((n) => n.id !== id)
109
118
  saveNotes(next)
@@ -142,7 +151,7 @@ export function Notes({
142
151
  </div>
143
152
  ) : notes.length === 0 ? (
144
153
  <div style={{ color: '#888', fontSize: Math.round(6 * scale), textAlign: 'center', padding: `${4 * scale}px` }}>
145
- No tasks yet
154
+ {/* No tasks yet */}
146
155
  </div>
147
156
  ) : (
148
157
  notes.map((note) => (
@@ -187,50 +196,72 @@ export function Notes({
187
196
  boxSizing: 'border-box',
188
197
  // borderTop: `${scale}px solid #555555`,
189
198
  }}>
190
- <textarea
191
- value={draft}
192
- rows={2}
193
- onChange={(e) => {
194
- // Strip newlines — behave like a single-line input but with word wrap
195
- setDraft(e.target.value.replace(/[\r\n]/g, ''))
196
- }}
197
- onKeyDown={(e) => {
198
- if (e.code === 'Enter') { e.preventDefault(); addNote() }
199
- }}
200
- placeholder="Add task…"
201
- style={{
202
- flex: 1,
203
- minWidth: 0,
204
- resize: 'none',
205
- background: '#8b8b8b',
206
- border: `${scale}px solid #373737`,
207
- color: '#fff',
208
- fontSize: Math.round(6 * scale),
209
- padding: `${scale}px`,
210
- fontFamily: 'inherit',
211
- outline: 'none',
212
- lineHeight: 1.4,
213
- boxSizing: 'border-box',
214
- overflowY: 'hidden',
215
- }}
216
- />
217
- <button
218
- onClick={addNote}
219
- disabled={!draft.trim()}
220
- style={{
221
- background: '#404040',
222
- color: '#fff',
223
- border: `${scale}px solid #666`,
224
- cursor: draft.trim() ? 'pointer' : 'not-allowed',
225
- fontSize: Math.round(6 * scale),
226
- padding: `${scale}px ${2 * scale}px`,
227
- fontFamily: 'inherit',
228
- flexShrink: 0,
229
- opacity: draft.trim() ? 1 : 0.5,
230
- }}
231
- >
232
- +
233
- </button>
199
+ {showInput && (
200
+ <textarea
201
+ value={draft}
202
+ rows={2}
203
+ autoFocus
204
+ onChange={(e) => {
205
+ setDraft(e.target.value.replace(/[\r\n]/g, ''))
206
+ }}
207
+ onKeyDown={(e) => {
208
+ if (e.code === 'Enter') { e.preventDefault(); addNote() }
209
+ if (e.code === 'Escape') { setShowInput(false); setDraft('') }
210
+ }}
211
+ placeholder="Add task…"
212
+ style={{
213
+ flex: 1,
214
+ minWidth: 0,
215
+ resize: 'none',
216
+ background: '#8b8b8b',
217
+ border: `${scale}px solid #373737`,
218
+ color: '#fff',
219
+ fontSize: Math.round(6 * scale),
220
+ padding: `${scale}px`,
221
+ fontFamily: 'inherit',
222
+ outline: 'none',
223
+ lineHeight: 1.4,
224
+ boxSizing: 'border-box',
225
+ overflowY: 'hidden',
226
+ }}
227
+ />
228
+ )}
229
+ <div style={{ display: 'flex', flexDirection: 'column', gap: scale, flexShrink: 0 }}>
230
+ {showInput && (
231
+ <button
232
+ onClick={() => { setShowInput(false); setDraft('') }}
233
+ title="Hide"
234
+ style={{
235
+ background: '#5a3a3a',
236
+ color: '#ccc',
237
+ border: `${scale}px solid #666`,
238
+ cursor: 'pointer',
239
+ fontSize: Math.round(6 * scale),
240
+ padding: `${scale}px ${2 * scale}px`,
241
+ fontFamily: 'inherit',
242
+ lineHeight: 1,
243
+ }}
244
+ >
245
+ ×
246
+ </button>
247
+ )}
248
+ <button
249
+ onClick={handleAddButtonClick}
250
+ disabled={showInput && !draft.trim()}
251
+ style={{
252
+ background: '#404040',
253
+ color: '#fff',
254
+ border: `${scale}px solid #666`,
255
+ cursor: showInput && !draft.trim() ? 'not-allowed' : 'pointer',
256
+ fontSize: Math.round(6 * scale),
257
+ padding: `${scale}px ${2 * scale}px`,
258
+ fontFamily: 'inherit',
259
+ opacity: showInput && !draft.trim() ? 0.5 : 1,
260
+ }}
261
+ >
262
+ +
263
+ </button>
264
+ </div>
234
265
  </div>
235
266
  </div>
236
267
  )
@@ -244,6 +244,9 @@ export function Slot({
244
244
  const touch = e.changedTouches[0]
245
245
  if (Math.abs(touch.clientX - start.x) > 10 || Math.abs(touch.clientY - start.y) > 10) return
246
246
  e.stopPropagation()
247
+ // Prevent the browser from firing a synthetic click after touchEnd.
248
+ // Without this, the click bubbles to the inventory window div which clears focusedSlot.
249
+ e.preventDefault()
247
250
 
248
251
  if (pKeyActive) setPKeyActive(false)
249
252
 
@@ -319,6 +322,9 @@ export function Slot({
319
322
  .filter(Boolean)
320
323
  .join(' ')}
321
324
  tabIndex={index >= 0 ? 0 : undefined}
325
+ data-slot={index}
326
+ data-debug={item?.debugKey ?? undefined}
327
+ data-texture={item?.textureKey ?? undefined}
322
328
  style={{
323
329
  width: renderSize,
324
330
  height: renderSize,
@@ -1,5 +1,6 @@
1
1
  import type { InventoryAction, InventoryWindowState, PlayerState, SlotState, ItemStack } from '../types'
2
2
  import type { InventoryConnector, ConnectorListener, ConnectorEvent } from './types'
3
+ import { isItemEqual, getMaxStackSize } from '../utils/isItemEqual'
3
4
 
4
5
  export interface ActionLogEntry {
5
6
  id: number
@@ -154,11 +155,75 @@ export function createDemoConnector(options: DemoConnectorOptions): InventoryCon
154
155
  }
155
156
  } else if (action.mode === 'shift' && slotState?.item) {
156
157
  // Simulate shift-click: just log it
158
+ } else if (action.mode === 'double') {
159
+ // Double-click collect: pick up all matching items from other slots up to maxStack
160
+ const held = windowState.heldItem
161
+ if (held) {
162
+ const maxStack = getMaxStackSize(held)
163
+ let remaining = maxStack - held.count
164
+ for (let i = 0; i < slots.length && remaining > 0; i++) {
165
+ const s = slots[i]
166
+ if (!s.item || !isItemEqual(s.item, held)) continue
167
+ const take = Math.min(s.item.count, remaining)
168
+ slots[i] = { ...s, item: s.item.count - take > 0 ? { ...s.item, count: s.item.count - take } : null }
169
+ remaining -= take
170
+ }
171
+ windowState = { ...windowState, slots, heldItem: { ...held, count: maxStack - remaining } }
172
+ }
157
173
  }
158
174
 
159
175
  emit({ type: 'windowUpdate', state: windowState })
160
176
  }
161
177
 
178
+ // Demo: simulate drag/spread behavior
179
+ if (action.type === 'drag' && windowState && windowState.heldItem) {
180
+ const held = windowState.heldItem
181
+ const maxStack = getMaxStackSize(held)
182
+ const slots = [...windowState.slots]
183
+
184
+ if (action.button === 'left') {
185
+ // Vanilla left-drag: distribute perSlot items evenly, remainder stays in cursor
186
+ const compatibleSlots = action.slots.filter((idx) => {
187
+ const existing = slots.find((s) => s.index === idx)?.item
188
+ return !existing || isItemEqual(existing, held)
189
+ })
190
+ if (compatibleSlots.length > 0) {
191
+ const perSlot = Math.floor(held.count / compatibleSlots.length)
192
+ // If perSlot=0 (more slots than items), nothing is distributed
193
+ if (perSlot > 0) {
194
+ let totalPlaced = 0
195
+ for (const idx of compatibleSlots) {
196
+ const si = slots.findIndex((s) => s.index === idx)
197
+ const existingCount = si >= 0 ? (slots[si].item?.count ?? 0) : 0
198
+ const add = Math.min(perSlot, maxStack - existingCount)
199
+ totalPlaced += add
200
+ const newCount = existingCount + add
201
+ if (si >= 0) slots[si] = { ...slots[si], item: { ...held, count: newCount } }
202
+ else slots.push({ index: idx, item: { ...held, count: newCount } })
203
+ }
204
+ const remaining = held.count - totalPlaced
205
+ windowState = { ...windowState, slots, heldItem: remaining > 0 ? { ...held, count: remaining } : null }
206
+ }
207
+ }
208
+ } else {
209
+ // Place 1 item per slot (right-click drag)
210
+ let remaining = held.count
211
+ for (const idx of action.slots) {
212
+ if (remaining <= 0) break
213
+ const si = slots.findIndex((s) => s.index === idx)
214
+ const existing = si >= 0 ? slots[si].item : null
215
+ if (existing && !isItemEqual(existing, held)) continue
216
+ const existingCount = existing?.count ?? 0
217
+ const newCount = Math.min(existingCount + 1, maxStack)
218
+ if (si >= 0) slots[si] = { ...slots[si], item: { ...held, count: newCount } }
219
+ else slots.push({ index: idx, item: { ...held, count: newCount } })
220
+ remaining--
221
+ }
222
+ windowState = { ...windowState, slots, heldItem: remaining > 0 ? { ...held, count: remaining } : null }
223
+ }
224
+ emit({ type: 'windowUpdate', state: windowState })
225
+ }
226
+
162
227
  if (action.type === 'drop' && windowState) {
163
228
  const slots = [...windowState.slots]
164
229
  const slotState = slots.find((s) => s.index === action.slotIndex)
@@ -35,6 +35,9 @@ function makeSlotConverter(itemMapper?: MineflayerConnectorOptions['itemMapper']
35
35
  count: slot.count,
36
36
  metadata: slot.metadata,
37
37
  nbt: slot.nbt as Record<string, unknown> | undefined,
38
+ // Default debug key: "<type>:<metadata>" — visible as data-debug on slot elements.
39
+ // Override via itemMapper if needed.
40
+ debugKey: slot.metadata ? `${slot.type}:${slot.metadata}` : String(slot.type),
38
41
  }
39
42
  return itemMapper ? itemMapper(slot, mapped) : mapped
40
43
  }
@@ -137,12 +137,11 @@ export function InventoryProvider({ connector, children, noDragSpread = false }:
137
137
  })
138
138
  if (compatibleSlots.length === 0) return preview
139
139
  const perSlot = Math.floor(held.count / compatibleSlots.length)
140
- let remainder = held.count % compatibleSlots.length
140
+ // Vanilla behavior: if perSlot=0 (more slots than items), nothing is distributed
141
+ if (perSlot === 0) return preview
141
142
  for (const idx of compatibleSlots) {
142
143
  const existingCount = ws?.slots.find((s) => s.index === idx)?.item?.count ?? 0
143
- const add = perSlot + (remainder > 0 ? 1 : 0)
144
- if (remainder > 0) remainder--
145
- const total = Math.min(existingCount + add, maxStack)
144
+ const total = Math.min(existingCount + perSlot, maxStack)
146
145
  preview.set(idx, { count: total })
147
146
  }
148
147
  } else {
@@ -197,21 +196,26 @@ export function InventoryProvider({ connector, children, noDragSpread = false }:
197
196
  })
198
197
  if (compatibleSlots.length > 0) {
199
198
  const perSlot = Math.floor(held.count / compatibleSlots.length)
200
- let remainder = held.count % compatibleSlots.length
201
- for (const idx of compatibleSlots) {
202
- const existingIdx = newSlots.findIndex((s) => s.index === idx)
203
- const existingCount = existingIdx >= 0 ? (newSlots[existingIdx].item?.count ?? 0) : 0
204
- const add = perSlot + (remainder > 0 ? 1 : 0)
205
- if (remainder > 0) remainder--
206
- const newCount = Math.min(existingCount + add, maxStack)
207
- if (existingIdx >= 0) {
208
- newSlots[existingIdx] = { index: idx, item: { ...held, count: newCount } }
209
- } else {
210
- newSlots.push({ index: idx, item: { ...held, count: newCount } })
199
+ // Vanilla behavior: if perSlot=0 (more slots than items), nothing is distributed
200
+ if (perSlot > 0) {
201
+ let totalPlaced = 0
202
+ for (const idx of compatibleSlots) {
203
+ const existingIdx = newSlots.findIndex((s) => s.index === idx)
204
+ const existingCount = existingIdx >= 0 ? (newSlots[existingIdx].item?.count ?? 0) : 0
205
+ const add = Math.min(perSlot, maxStack - existingCount)
206
+ totalPlaced += add
207
+ const newCount = existingCount + add
208
+ if (existingIdx >= 0) {
209
+ newSlots[existingIdx] = { index: idx, item: { ...held, count: newCount } }
210
+ } else {
211
+ newSlots.push({ index: idx, item: { ...held, count: newCount } })
212
+ }
211
213
  }
214
+ if (ws) setWindowState({ ...ws, slots: newSlots })
215
+ const remaining = held.count - totalPlaced
216
+ if (remaining > 0) setHeldItemState({ ...held, count: remaining })
217
+ else setHeldItemState(null)
212
218
  }
213
- if (ws) setWindowState({ ...ws, slots: newSlots })
214
- setHeldItemState(null)
215
219
  }
216
220
  } else {
217
221
  // Right-click drag: place 1 per slot
@@ -256,6 +260,19 @@ export function InventoryProvider({ connector, children, noDragSpread = false }:
256
260
  [windowState],
257
261
  )
258
262
 
263
+ // Expose state to window for easier debugging from browser DevTools.
264
+ // Access via: window.__mcInv
265
+ useEffect(() => {
266
+ ;(window as unknown as Record<string, unknown>).__mcInv = {
267
+ get windowState() { return windowStateRef.current },
268
+ get heldItem() { return heldItemRef.current },
269
+ get dragSlots() { return dragSlots },
270
+ get isDragging() { return isDragging },
271
+ get focusedSlot() { return focusedSlot },
272
+ get pKeyActive() { return pKeyActive },
273
+ }
274
+ })
275
+
259
276
  const value: InventoryContextValue = {
260
277
  windowState,
261
278
  playerState,
package/src/index.tsx CHANGED
@@ -49,7 +49,7 @@ export type {
49
49
  export type { ActionLogEntry, DemoConnectorOptions } from './connector/demo'
50
50
 
51
51
  // Registry
52
- export { registerInventoryType, getInventoryType, getAllInventoryTypes } from './registry'
52
+ export { registerInventoryType, registerTypeAlias, getInventoryType, getAllInventoryTypes } from './registry'
53
53
  export type { InventoryTypeDefinition, WindowType } from './registry'
54
54
 
55
55
  // Types
@@ -5,12 +5,41 @@ const registry = new Map<string, InventoryTypeDefinition>(
5
5
  Object.entries(inventoryDefinitions),
6
6
  )
7
7
 
8
+ /**
9
+ * Maps alternative names/aliases to canonical inventory type names.
10
+ * Both the alias and the canonical name resolve to the same definition.
11
+ *
12
+ * Add entries here to support shorthand or legacy type identifiers.
13
+ */
14
+ const typeAliases: Record<string, string> = {
15
+ // Shorthand aliases
16
+ crafting3x3: 'crafting_table',
17
+ crafting: 'crafting_table',
18
+ chest: 'chest', // already canonical, listed for clarity
19
+ // Protocol-level minecraft: prefix stripping is handled in getInventoryType
20
+ }
21
+
8
22
  export function registerInventoryType(def: InventoryTypeDefinition): void {
9
23
  registry.set(def.name, def)
10
24
  }
11
25
 
26
+ /**
27
+ * Resolve an inventory type name or alias to its definition.
28
+ * Handles:
29
+ * - Exact matches (e.g. "crafting_table")
30
+ * - Aliases defined in `typeAliases` (e.g. "crafting3x3" → "crafting_table")
31
+ * - "minecraft:" namespace prefix (e.g. "minecraft:generic_9x3" → "generic_9x3")
32
+ */
12
33
  export function getInventoryType(name: string): InventoryTypeDefinition | undefined {
13
- return registry.get(name)
34
+ // Strip "minecraft:" namespace prefix if present
35
+ const stripped = name.startsWith('minecraft:') ? name.slice('minecraft:'.length) : name
36
+ // Resolve alias if defined
37
+ const canonical = typeAliases[stripped] ?? stripped
38
+ return registry.get(canonical)
39
+ }
40
+
41
+ export function registerTypeAlias(alias: string, canonical: string): void {
42
+ typeAliases[alias] = canonical
14
43
  }
15
44
 
16
45
  export function getAllInventoryTypes(): InventoryTypeDefinition[] {
@@ -127,15 +127,16 @@ export const inventoryDefinitions = makeInventoryDefinitions({
127
127
 
128
128
  // generic_9x1..9x6 all use generic_54.png (the 6-row chest texture, 176×222).
129
129
  // InventoryBackground canvas-stitches rows < 6: takes the top N rows from the source
130
- // then the player-inventory section (bottom 96px) and composes them into the output.
131
- // Slot formula: containerRows=N → container at y=18, player at y = N*18+30.
132
- // backgroundHeight = N*18 + 113 (96px player section + 17px title).
130
+ // (y=0..topH-1, where topH = N*18+17) then the player-inventory section starting at
131
+ // y=SRC_PLAYER_Y=125 and composites them.
132
+ // Output height formula: N*18+17 + (222-125) = N*18+17+97 = N*18+114.
133
+ // Slot formula: container at y=18, player at y = N*18+30 (13px frame into player section).
133
134
  generic_9x1: {
134
135
  name: 'generic_9x1',
135
136
  title: 'Container',
136
137
  backgroundTexture: '1.21.11/textures/gui/container/generic_54.png',
137
138
  backgroundWidth: 176,
138
- backgroundHeight: 131, // 1*18 + 113
139
+ backgroundHeight: 132, // 1*18 + 114
139
140
  containerRows: 1,
140
141
  slots: [
141
142
  ...gridSlots(9, 1, 8, 18, 'container'),
@@ -148,7 +149,7 @@ export const inventoryDefinitions = makeInventoryDefinitions({
148
149
  title: 'Container',
149
150
  backgroundTexture: '1.21.11/textures/gui/container/generic_54.png',
150
151
  backgroundWidth: 176,
151
- backgroundHeight: 149, // 2*18 + 113
152
+ backgroundHeight: 150, // 2*18 + 114
152
153
  containerRows: 2,
153
154
  slots: [
154
155
  ...gridSlots(9, 2, 8, 18, 'container'),
@@ -161,7 +162,7 @@ export const inventoryDefinitions = makeInventoryDefinitions({
161
162
  title: 'Container',
162
163
  backgroundTexture: '1.21.11/textures/gui/container/generic_54.png',
163
164
  backgroundWidth: 176,
164
- backgroundHeight: 167, // 3*18 + 113
165
+ backgroundHeight: 168, // 3*18 + 114
165
166
  containerRows: 3,
166
167
  playerInventoryOffset: { x: 8, y: 84 },
167
168
  slots: [
@@ -175,7 +176,7 @@ export const inventoryDefinitions = makeInventoryDefinitions({
175
176
  title: 'Container',
176
177
  backgroundTexture: '1.21.11/textures/gui/container/generic_54.png',
177
178
  backgroundWidth: 176,
178
- backgroundHeight: 185, // 4*18 + 113
179
+ backgroundHeight: 186, // 4*18 + 114
179
180
  containerRows: 4,
180
181
  slots: [
181
182
  ...gridSlots(9, 4, 8, 18, 'container'),
@@ -188,7 +189,7 @@ export const inventoryDefinitions = makeInventoryDefinitions({
188
189
  title: 'Container',
189
190
  backgroundTexture: '1.21.11/textures/gui/container/generic_54.png',
190
191
  backgroundWidth: 176,
191
- backgroundHeight: 203, // 5*18 + 113
192
+ backgroundHeight: 204, // 5*18 + 114
192
193
  containerRows: 5,
193
194
  slots: [
194
195
  ...gridSlots(9, 5, 8, 18, 'container'),
@@ -201,7 +202,7 @@ export const inventoryDefinitions = makeInventoryDefinitions({
201
202
  title: 'Container',
202
203
  backgroundTexture: '1.21.11/textures/gui/container/generic_54.png',
203
204
  backgroundWidth: 176,
204
- backgroundHeight: 222, // full 6-row texture, no stitching needed
205
+ backgroundHeight: 222, // full 6-row texture, no stitching needed (N*18+114 = 222)
205
206
  playerInventoryOffset: { x: 8, y: 140 },
206
207
  slots: [
207
208
  ...gridSlots(9, 6, 8, 18, 'container'),
package/src/types.ts CHANGED
@@ -19,6 +19,12 @@ export interface ItemStack {
19
19
  * Example: `"item/dye_black"` or `"entity/spider/spider"`
20
20
  */
21
21
  textureKey?: string
22
+ /**
23
+ * Arbitrary debug identifier exposed as a `data-debug` attribute on the slot element.
24
+ * The mineflayer connector sets this to `"<type>:<metadata>"` by default, making it easy
25
+ * to inspect raw item IDs in browser DevTools without touching React internals.
26
+ */
27
+ debugKey?: string
22
28
  }
23
29
 
24
30
  export interface SlotState {