minecraft-inventory 0.1.3 → 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.
@@ -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
  )
@@ -42,12 +42,17 @@ export function Slot({
42
42
  isDragging,
43
43
  dragSlots,
44
44
  dragButton,
45
+ dragPreview,
45
46
  startDrag,
46
47
  addDragSlot,
47
48
  endDrag,
48
49
  hoveredSlot,
49
50
  setHoveredSlot,
50
51
  activeNumberKey,
52
+ pKeyActive,
53
+ setPKeyActive,
54
+ focusedSlot,
55
+ setFocusedSlot,
51
56
  } = useInventoryContext()
52
57
 
53
58
  const { contentSize } = useScale()
@@ -89,6 +94,10 @@ export function Slot({
89
94
 
90
95
  const isHovered = hoveredSlot === index
91
96
  const isDragTarget = dragSlots.includes(index)
97
+ const dragPreviewEntry = dragPreview.get(index)
98
+ const isFocused = focusedSlot === index
99
+ const showPKeyNumber = pKeyActive && index >= 0 && index <= 99
100
+ const isInFocusSwapMode = focusedSlot !== null || pKeyActive
92
101
 
93
102
  // Keyboard number key while hovering
94
103
  useEffect(() => {
@@ -129,22 +138,44 @@ export function Slot({
129
138
  (e: React.MouseEvent) => {
130
139
  if (isMobile || disabled) return
131
140
  e.preventDefault()
141
+ e.stopPropagation()
132
142
  const button = e.button === 2 ? 'right' : e.button === 1 ? 'middle' : 'left'
133
143
  if (isDragging && dragSlots.length > 1) {
134
144
  endDrag()
135
145
  return
136
146
  }
147
+
148
+ // Focus/swap logic — active in P mode OR when a slot is already focused
149
+ if (button === 'left' && (pKeyActive || focusedSlot !== null)) {
150
+ if (pKeyActive) setPKeyActive(false)
151
+ if (focusedSlot === null) {
152
+ setFocusedSlot(index)
153
+ } else if (focusedSlot === index) {
154
+ setFocusedSlot(null)
155
+ } else {
156
+ sendAction({ type: 'click', slotIndex: focusedSlot, button: 'left', mode: 'normal' })
157
+ sendAction({ type: 'click', slotIndex: index, button: 'left', mode: 'normal' })
158
+ sendAction({ type: 'click', slotIndex: focusedSlot, button: 'left', mode: 'normal' })
159
+ setFocusedSlot(null)
160
+ }
161
+ if (isDragging) endDrag()
162
+ return
163
+ }
164
+
137
165
  const mode = e.shiftKey ? 'shift' : 'normal'
138
166
  if (onClickOverride) {
139
167
  onClickOverride(button, mode)
140
168
  } else {
141
- // Hide tooltip immediately when picking up or placing an item
142
- if (button === 'left' || button === 'right') setShowTooltip(false)
143
- sendAction({ type: 'click', slotIndex: index, button, mode })
169
+ if (resultSlot && heldItem && !item && mode === 'normal') {
170
+ // Cannot place items into result/output slots
171
+ } else {
172
+ if (button === 'left' || button === 'right') setShowTooltip(false)
173
+ sendAction({ type: 'click', slotIndex: index, button, mode })
174
+ }
144
175
  }
145
176
  if (isDragging) endDrag()
146
177
  },
147
- [isMobile, disabled, isDragging, dragSlots.length, sendAction, index, endDrag, onClickOverride],
178
+ [isMobile, disabled, isDragging, dragSlots.length, sendAction, index, endDrag, onClickOverride, resultSlot, heldItem, item, pKeyActive, setPKeyActive, focusedSlot, setFocusedSlot],
148
179
  )
149
180
 
150
181
  const handleDoubleClick = useCallback(
@@ -212,17 +243,33 @@ export function Slot({
212
243
  touchStartRef.current = null
213
244
  const touch = e.changedTouches[0]
214
245
  if (Math.abs(touch.clientX - start.x) > 10 || Math.abs(touch.clientY - start.y) > 10) return
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()
250
+
251
+ if (pKeyActive) setPKeyActive(false)
215
252
 
216
253
  if (heldItem) {
254
+ // When holding an item, place it (standard behavior, no focus needed)
217
255
  sendAction({ type: 'click', slotIndex: index, button: 'left', mode: 'normal' })
218
- } else if (item) {
219
- const rect = slotRef.current?.getBoundingClientRect()
220
- if (rect) setMobileTouchPos({ x: rect.right, y: rect.top })
221
- setMobileMenuOpen(true)
222
- setShowTooltip(true)
256
+ return
257
+ }
258
+
259
+ // On mobile, tapping always uses the focus/swap mechanism:
260
+ // first tap focuses, second tap on a different slot swaps, same slot clears.
261
+ if (focusedSlot === null) {
262
+ setFocusedSlot(index)
263
+ } else if (focusedSlot === index) {
264
+ setFocusedSlot(null)
265
+ } else {
266
+ sendAction({ type: 'click', slotIndex: focusedSlot, button: 'left', mode: 'normal' })
267
+ sendAction({ type: 'click', slotIndex: index, button: 'left', mode: 'normal' })
268
+ sendAction({ type: 'click', slotIndex: focusedSlot, button: 'left', mode: 'normal' })
269
+ setFocusedSlot(null)
223
270
  }
224
271
  },
225
- [isMobile, disabled, heldItem, item, sendAction, index],
272
+ [isMobile, disabled, heldItem, sendAction, index, pKeyActive, setPKeyActive, focusedSlot, setFocusedSlot],
226
273
  )
227
274
 
228
275
  const handleMobilePickAll = useCallback(() => {
@@ -274,11 +321,16 @@ export function Slot({
274
321
  ]
275
322
  .filter(Boolean)
276
323
  .join(' ')}
324
+ tabIndex={index >= 0 ? 0 : undefined}
325
+ data-slot={index}
326
+ data-debug={item?.debugKey ?? undefined}
327
+ data-texture={item?.textureKey ?? undefined}
277
328
  style={{
278
329
  width: renderSize,
279
330
  height: renderSize,
280
331
  position: 'relative',
281
332
  flexShrink: 0,
333
+ ...(isFocused ? { outline: `2px dashed #ff0`, outlineOffset: -2, animation: 'mc-inv-focus-dash 0.5s linear infinite' } : {}),
282
334
  ...style,
283
335
  }}
284
336
  onMouseEnter={handleMouseEnter}
@@ -314,7 +366,58 @@ export function Slot({
314
366
  </div>
315
367
  )}
316
368
 
317
- {item && showTooltip && !mobileMenuOpen && (
369
+ {showPKeyNumber && (
370
+ <div
371
+ className="mc-inv-pkey-overlay"
372
+ style={{
373
+ position: 'absolute',
374
+ inset: 0,
375
+ background: 'rgba(255, 0, 0, 0.2)',
376
+ border: '1px solid rgba(255, 0, 0, 0.5)',
377
+ display: 'flex',
378
+ alignItems: 'center',
379
+ justifyContent: 'center',
380
+ paddingLeft: 4,
381
+ pointerEvents: 'none',
382
+ zIndex: 4,
383
+ boxSizing: 'border-box',
384
+ }}
385
+ >
386
+ <span
387
+ style={{
388
+ fontSize: Math.round(renderSize * 0.4),
389
+ fontFamily: "'Minecraftia', 'Minecraft', monospace",
390
+ color: '#ffffff',
391
+ textShadow: '1px 1px 0 rgba(0,0,0,0.7)',
392
+ lineHeight: 1,
393
+ }}
394
+ >
395
+ {String(index).padStart(2, '0')}
396
+ </span>
397
+ </div>
398
+ )}
399
+
400
+ {dragPreviewEntry && (
401
+ <div
402
+ className="mc-inv-drag-preview-count"
403
+ style={{
404
+ position: 'absolute',
405
+ right: 1,
406
+ bottom: 1,
407
+ fontSize: Math.round(renderSize * 0.45),
408
+ fontFamily: "'Minecraftia', 'Minecraft', monospace",
409
+ color: '#ffff00',
410
+ textShadow: '1px 1px 0 #3f3f00',
411
+ lineHeight: 1,
412
+ pointerEvents: 'none',
413
+ zIndex: 3,
414
+ }}
415
+ >
416
+ {dragPreviewEntry.count}
417
+ </div>
418
+ )}
419
+
420
+ {item && showTooltip && !mobileMenuOpen && !heldItem && (
318
421
  <Tooltip item={item} visible />
319
422
  )}
320
423
 
@@ -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)
@@ -1,20 +1,55 @@
1
1
  import type { InventoryAction, InventoryWindowState, PlayerState, SlotState, ItemStack } from '../types'
2
2
  import type { InventoryConnector, ConnectorListener, ConnectorEvent, MineflayerBot } from './types'
3
3
 
4
- function botSlotToItemStack(slot: MineflayerBot['heldItem']): ItemStack | null {
5
- if (!slot || slot.type === -1 || slot.type === 0) return null
6
- return {
7
- type: slot.type,
8
- count: slot.count,
9
- metadata: slot.metadata,
10
- nbt: slot.nbt as Record<string, unknown> | undefined,
4
+ type RawSlot = { type: number; count: number; metadata?: number; nbt?: unknown }
5
+
6
+ /**
7
+ * Options for {@link createMineflayerConnector}.
8
+ */
9
+ export interface MineflayerConnectorOptions {
10
+ /**
11
+ * Custom item mapper called for every slot conversion from raw mineflayer data to
12
+ * {@link ItemStack}. Receives the raw slot data and the default-mapped stack.
13
+ * Return a modified stack to override fields (e.g. `name`, `textureKey`, `displayName`),
14
+ * or return the second argument unchanged to use the default mapping.
15
+ *
16
+ * @example
17
+ * ```ts
18
+ * createMineflayerConnector(bot, {
19
+ * itemMapper: (raw, mapped) => ({
20
+ * ...mapped,
21
+ * // Override texture for specific numeric type IDs:
22
+ * textureKey: raw.type === 438 ? 'item/potion_water' : mapped.textureKey,
23
+ * }),
24
+ * })
25
+ * ```
26
+ */
27
+ itemMapper?: (raw: RawSlot, mapped: ItemStack) => ItemStack
28
+ }
29
+
30
+ function makeSlotConverter(itemMapper?: MineflayerConnectorOptions['itemMapper']) {
31
+ return function botSlotToItemStack(slot: RawSlot | null | undefined): ItemStack | null {
32
+ if (!slot || slot.type === -1 || slot.type === 0) return null
33
+ const mapped: ItemStack = {
34
+ type: slot.type,
35
+ count: slot.count,
36
+ metadata: slot.metadata,
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),
41
+ }
42
+ return itemMapper ? itemMapper(slot, mapped) : mapped
11
43
  }
12
44
  }
13
45
 
14
- function botSlotsToSlotStates(slots: MineflayerBot['inventory']['slots']): SlotState[] {
46
+ function botSlotsToSlotStates(
47
+ slots: MineflayerBot['inventory']['slots'],
48
+ convert: (slot: RawSlot | null | undefined) => ItemStack | null,
49
+ ): SlotState[] {
15
50
  return slots.map((slot, index) => ({
16
51
  index,
17
- item: botSlotToItemStack(slot),
52
+ item: convert(slot),
18
53
  }))
19
54
  }
20
55
 
@@ -67,23 +102,47 @@ function isBeaconWindow(win: unknown): win is { setBeaconEffects?: (primary: num
67
102
  return win != null && (typeof (win as Record<string, unknown>).setBeaconEffects === 'function' || /beacon/i.test(String((win as Record<string, unknown>).type)))
68
103
  }
69
104
 
70
- export function createMineflayerConnector(bot: MineflayerBot): InventoryConnector {
105
+ export function createMineflayerConnector(bot: MineflayerBot, options?: MineflayerConnectorOptions): InventoryConnector {
71
106
  const listeners = new Set<ConnectorListener>()
72
107
  const ext = bot as MineflayerBotExtended
108
+ const convert = makeSlotConverter(options?.itemMapper)
73
109
 
74
110
  function emit(event: ConnectorEvent) {
75
111
  listeners.forEach((l) => l(event))
76
112
  }
77
113
 
114
+ /**
115
+ * Builds a window state from the currently open window, OR from `bot.inventory`
116
+ * when no container is open (exposing the player's own inventory as a synthetic
117
+ * 'player' window with windowId = 0).
118
+ */
78
119
  function buildWindowState(): InventoryWindowState | null {
79
120
  const win = bot.currentWindow
80
- if (!win) return null
121
+ if (win) {
122
+ return {
123
+ windowId: win.id,
124
+ type: win.type ?? 'unknown',
125
+ title: win.title,
126
+ slots: botSlotsToSlotStates(win.slots, convert),
127
+ heldItem: convert(bot.heldItem),
128
+ }
129
+ }
130
+ // No open container — expose the player inventory as a synthetic 'player' window.
131
+ const invSlots: SlotState[] = []
132
+ // Slots 0–8: crafting/armour — leave empty (not accessible from bot.inventory directly)
133
+ for (let i = 0; i < 9; i++) invSlots.push({ index: i, item: null })
134
+ // Slots 9–35: main inventory
135
+ for (let i = 9; i <= 35; i++) invSlots.push({ index: i, item: convert(bot.inventory.slots[i]) })
136
+ // Slots 36–44: hotbar
137
+ for (let i = 36; i <= 44; i++) invSlots.push({ index: i, item: convert(bot.inventory.slots[i]) })
138
+ // Slot 45: offhand
139
+ invSlots.push({ index: 45, item: convert(bot.inventory.slots[45]) })
81
140
  return {
82
- windowId: win.id,
83
- type: win.type ?? 'unknown',
84
- title: win.title,
85
- slots: botSlotsToSlotStates(win.slots),
86
- heldItem: botSlotToItemStack(bot.heldItem),
141
+ windowId: 0,
142
+ type: 'player',
143
+ title: undefined,
144
+ slots: invSlots,
145
+ heldItem: convert(bot.heldItem),
87
146
  }
88
147
  }
89
148
 
@@ -91,7 +150,7 @@ export function createMineflayerConnector(bot: MineflayerBot): InventoryConnecto
91
150
  const inv = bot.inventory.slots
92
151
  return {
93
152
  activeHotbarSlot: bot.quickBarSlot,
94
- inventory: botSlotsToSlotStates(inv),
153
+ inventory: botSlotsToSlotStates(inv, convert),
95
154
  }
96
155
  }
97
156
 
@@ -135,21 +194,20 @@ export function createMineflayerConnector(bot: MineflayerBot): InventoryConnecto
135
194
  // Llama inventory structure (per registry):
136
195
  // Slot 0: Carpet (saddle) - empty since we don't have entity data
137
196
  slots.push({ index: 0, item: null })
138
- // Slot 1: skipped (not used)
139
197
  // Slots 2-16: Llama chest (5×3 grid = 15 slots) - empty since we don't have entity data
140
198
  for (let i = 2; i <= 16; i++) {
141
199
  slots.push({ index: i, item: null })
142
200
  }
143
201
  // Slots 17-43: Player inventory (bot.inventory.slots indices 9-35 map to window slots 17-43)
144
202
  for (let i = 9; i <= 35; i++) {
145
- slots.push({ index: i + 8, item: botSlotToItemStack(bot.inventory.slots[i]) })
203
+ slots.push({ index: i + 8, item: convert(bot.inventory.slots[i]) })
146
204
  }
147
205
  // Slots 44-52: Hotbar (bot.inventory.slots indices 36-44 map to window slots 44-52)
148
206
  for (let i = 36; i <= 44; i++) {
149
- slots.push({ index: i + 8, item: botSlotToItemStack(bot.inventory.slots[i]) })
207
+ slots.push({ index: i + 8, item: convert(bot.inventory.slots[i]) })
150
208
  }
151
209
  // Slot 53: Offhand (bot.inventory slot 45 maps to window slot 53)
152
- slots.push({ index: 53, item: botSlotToItemStack(bot.inventory.slots[45]) })
210
+ slots.push({ index: 53, item: convert(bot.inventory.slots[45]) })
153
211
  } else {
154
212
  // Player inventory window structure (per registry):
155
213
  // Slots 0-8: Crafting result + grid + armor (not in bot.inventory.slots, leave empty)
@@ -158,14 +216,14 @@ export function createMineflayerConnector(bot: MineflayerBot): InventoryConnecto
158
216
  }
159
217
  // Slots 9-35: Player inventory (bot.inventory.slots indices 9-35)
160
218
  for (let i = 9; i <= 35; i++) {
161
- slots.push({ index: i, item: botSlotToItemStack(bot.inventory.slots[i]) })
219
+ slots.push({ index: i, item: convert(bot.inventory.slots[i]) })
162
220
  }
163
221
  // Slots 36-44: Hotbar (bot.inventory.slots indices 36-44)
164
222
  for (let i = 36; i <= 44; i++) {
165
- slots.push({ index: i, item: botSlotToItemStack(bot.inventory.slots[i]) })
223
+ slots.push({ index: i, item: convert(bot.inventory.slots[i]) })
166
224
  }
167
225
  // Slot 45: Offhand (bot.inventory slot 45)
168
- slots.push({ index: 45, item: botSlotToItemStack(bot.inventory.slots[45]) })
226
+ slots.push({ index: 45, item: convert(bot.inventory.slots[45]) })
169
227
  }
170
228
 
171
229
  const windowState: InventoryWindowState = {
@@ -173,7 +231,7 @@ export function createMineflayerConnector(bot: MineflayerBot): InventoryConnecto
173
231
  type: inventoryType,
174
232
  title: inventoryType === 'llama' ? 'Llama' : undefined,
175
233
  slots,
176
- heldItem: botSlotToItemStack(bot.heldItem),
234
+ heldItem: convert(bot.heldItem),
177
235
  }
178
236
 
179
237
  emit({ type: 'windowOpen', state: windowState })