minecraft-inventory 0.1.43 → 0.1.44

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.43",
3
+ "version": "0.1.44",
4
4
  "type": "module",
5
5
  "license": "MIT",
6
6
  "release": {
@@ -353,7 +353,7 @@ export function InventoryOverlay({
353
353
  lineHeight: 1,
354
354
  }}
355
355
  >
356
- INV 0.1.43
356
+ INV 0.1.44
357
357
  </a>
358
358
  )}
359
359
 
@@ -130,7 +130,7 @@ export function HotbarExtras({ showOffhand, container, offhandItem }: HotbarExtr
130
130
  />
131
131
  {/* Item at slot 45 (offhand) */}
132
132
  <div style={{ position: 'absolute', top: 4 * scale, left: 3 * scale }}>
133
- <Slot index={45} item={offhandItem} size={16 * scale} noBackground />
133
+ <Slot index={45} item={offhandItem} size={16 * scale} noBackground disableFocusSwap />
134
134
  </div>
135
135
  </div>
136
136
  )}
@@ -98,7 +98,6 @@ export function InventoryWindow({
98
98
  // Offset by borderPx to land inside the texture's slot cell (skip the 1px border)
99
99
  left: slotDef.x * scale + borderPx,
100
100
  top: slotDef.y * scale + borderPx,
101
- ...(isHotbar ? { pointerEvents: 'none' as const } : {}),
102
101
  }}
103
102
  >
104
103
  <Slot
@@ -107,6 +106,7 @@ export function InventoryWindow({
107
106
  size={slotDef.size ? slotDef.size * scale - 2 * borderPx : contentSize}
108
107
  resultSlot={slotDef.resultSlot}
109
108
  label={slotDef.label}
109
+ disableFocusSwap={isHotbar}
110
110
  />
111
111
  </div>
112
112
  ))}
@@ -22,6 +22,11 @@ interface RecipeInventoryViewProps {
22
22
  onPushFrame: (item: RecipeNavFrame['item'], mode: 'recipes' | 'usages') => void
23
23
  }
24
24
 
25
+ function minecraftWikiUrlForName(nameOrDisplay: string): string {
26
+ const slug = nameOrDisplay.replace(/ /g, '_')
27
+ return `https://minecraft.wiki/w/${encodeURIComponent(slug)}`
28
+ }
29
+
25
30
  /** Slot item with a Tooltip, used inside the recipe view */
26
31
  function RecipeItemCell({
27
32
  item,
@@ -29,12 +34,14 @@ function RecipeItemCell({
29
34
  y,
30
35
  size,
31
36
  onHover,
37
+ onRecipeNavigate,
32
38
  }: {
33
39
  item: ItemStack | null
34
40
  x: number
35
41
  y: number
36
42
  size: number
37
43
  onHover: (item: ItemStack | null) => void
44
+ onRecipeNavigate?: (item: ItemStack, mode: 'recipes' | 'usages') => void
38
45
  }) {
39
46
  const [hovered, setHovered] = useState(false)
40
47
 
@@ -63,6 +70,15 @@ function RecipeItemCell({
63
70
  setHovered(false)
64
71
  onHover(null)
65
72
  }}
73
+ onClick={(e) => {
74
+ e.stopPropagation()
75
+ onRecipeNavigate?.(item, 'recipes')
76
+ }}
77
+ onContextMenu={(e) => {
78
+ e.preventDefault()
79
+ e.stopPropagation()
80
+ onRecipeNavigate?.(item, 'usages')
81
+ }}
66
82
  >
67
83
  <ItemCanvas item={item} size={size} style={{ position: 'absolute', top: 0, left: 0, pointerEvents: 'none' }} />
68
84
  {hovered && <Tooltip item={item} visible />}
@@ -100,6 +116,24 @@ export function RecipeInventoryView({
100
116
  }
101
117
  }, [])
102
118
 
119
+ const handleRecipeItemNavigate = useCallback(
120
+ (item: ItemStack, mode: 'recipes' | 'usages') => {
121
+ onPushFrame(
122
+ {
123
+ type: item.type,
124
+ name: item.name ?? '',
125
+ displayName: item.displayName ?? item.name ?? `Item #${item.type}`,
126
+ count: item.count,
127
+ metadata: item.metadata,
128
+ },
129
+ mode,
130
+ )
131
+ },
132
+ [onPushFrame],
133
+ )
134
+
135
+ const wikiHref = minecraftWikiUrlForName(frame.item.name || frame.item.displayName || 'Unknown')
136
+
103
137
  // R / U for nested navigation on hovered recipe items
104
138
  useEffect(() => {
105
139
  const handler = (e: KeyboardEvent) => {
@@ -215,6 +249,22 @@ export function RecipeInventoryView({
215
249
  )}
216
250
  </span>
217
251
 
252
+ <a
253
+ href={wikiHref}
254
+ target="_blank"
255
+ rel="noopener noreferrer"
256
+ onMouseDown={(e) => e.stopPropagation()}
257
+ onClick={(e) => e.stopPropagation()}
258
+ style={{
259
+ color: '#3366bb',
260
+ textDecoration: 'underline',
261
+ flexShrink: 0,
262
+ whiteSpace: 'nowrap',
263
+ }}
264
+ >
265
+ Wiki
266
+ </a>
267
+
218
268
  {/* Prev / next for multiple guides */}
219
269
  {totalGuides > 1 && (
220
270
  <>
@@ -250,6 +300,7 @@ export function RecipeInventoryView({
250
300
  navPx={navPx}
251
301
  contentSize={contentSize}
252
302
  onHoverItem={handleHoverItem}
303
+ onRecipeNavigate={handleRecipeItemNavigate}
253
304
  />
254
305
  )}
255
306
 
@@ -263,7 +314,7 @@ export function RecipeInventoryView({
263
314
  color: 'rgba(80,80,80,0.7)',
264
315
  pointerEvents: 'none',
265
316
  }}>
266
- Hover ingredient + R / U for nested lookup
317
+ Hover ingredient + R / U, or left / right-click (usages)
267
318
  </div>
268
319
  )}
269
320
  </div>
@@ -283,6 +334,7 @@ function CroppedRecipeBackground({
283
334
  navPx,
284
335
  contentSize,
285
336
  onHoverItem,
337
+ onRecipeNavigate,
286
338
  }: {
287
339
  guide: RecipeGuide
288
340
  containerSlots: Array<{ index: number; x: number; y: number; size?: number; group?: string }>
@@ -291,6 +343,7 @@ function CroppedRecipeBackground({
291
343
  navPx: number
292
344
  contentSize: number
293
345
  onHoverItem: (item: ItemStack | null) => void
346
+ onRecipeNavigate?: (item: ItemStack, mode: 'recipes' | 'usages') => void
294
347
  }) {
295
348
  const textures = useTextures()
296
349
  const layoutType = guide.type === 'smelting' ? 'furnace' : 'crafting_table'
@@ -375,6 +428,7 @@ function CroppedRecipeBackground({
375
428
  y={slotDef.y * scale}
376
429
  size={slotDef.size ? slotDef.size * scale - 2 * navPx : contentSize}
377
430
  onHover={onHoverItem}
431
+ onRecipeNavigate={onRecipeNavigate}
378
432
  />
379
433
  )
380
434
  })}
@@ -406,8 +460,6 @@ function DescriptionCard({
406
460
  const fontSize = Math.max(6, Math.round(6 * scale))
407
461
  const iconSize = 16 * scale
408
462
 
409
- const wikiUrl = `https://minecraft.wiki/w/${encodeURIComponent(itemName.replace(/ /g, '_'))}`
410
-
411
463
  return (
412
464
  <div
413
465
  className="mc-inv-description-card"
@@ -504,25 +556,6 @@ function DescriptionCard({
504
556
  {guide.description}
505
557
  </div>
506
558
  )}
507
-
508
- {/* Minecraft Wiki link */}
509
- <a
510
- href={wikiUrl}
511
- target="_blank"
512
- rel="noopener noreferrer"
513
- style={{
514
- position: 'absolute',
515
- bottom: 4 * scale,
516
- right: 8 * scale,
517
- fontSize,
518
- fontFamily: "'Minecraftia', 'Minecraft', monospace",
519
- color: '#3366bb',
520
- textDecoration: 'underline',
521
- cursor: 'pointer',
522
- }}
523
- >
524
- Minecraft Wiki
525
- </a>
526
559
  </div>
527
560
  )
528
561
  }
@@ -7,6 +7,10 @@ import { Tooltip } from '../Tooltip'
7
7
  import { useMobile } from '../../hooks/useMobile'
8
8
  import styles from './Slot.module.css'
9
9
 
10
+ /** Hotbar HUD long-press: first threshold drops one item; hold longer for whole stack. */
11
+ const HOTBAR_LONG_PRESS_DROP_ONE_MS = 420
12
+ const HOTBAR_LONG_PRESS_DROP_ALL_EXTRA_MS = 600
13
+
10
14
  interface SlotProps {
11
15
  index: number
12
16
  item: ItemStack | null
@@ -19,6 +23,8 @@ interface SlotProps {
19
23
  style?: React.CSSProperties
20
24
  /** Remove slot background/border (e.g. for JEI items) */
21
25
  noBackground?: boolean
26
+ /** When true, skip P-key / focus-swap UI and mobile two-tap swap (e.g. standalone hotbar HUD). */
27
+ disableFocusSwap?: boolean
22
28
  /** Override default click behavior - when provided, calls this instead of sendAction */
23
29
  onClickOverride?: (button: 'left' | 'right' | 'middle', mode: 'normal' | 'shift' | 'double') => void
24
30
  }
@@ -34,6 +40,7 @@ export function Slot({
34
40
  className,
35
41
  style,
36
42
  noBackground,
43
+ disableFocusSwap = false,
37
44
  onClickOverride,
38
45
  }: SlotProps) {
39
46
  const {
@@ -96,6 +103,18 @@ export function Slot({
96
103
  }
97
104
  }, [label, item, renderSize])
98
105
 
106
+ // Mobile touch — timer ref must exist before cleanup effect below
107
+ const touchStartRef = useRef<{ x: number; y: number; time: number } | null>(null)
108
+ const longPressTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null)
109
+ const longPressFiredRef = useRef(false)
110
+
111
+ const cancelLongPress = useCallback(() => {
112
+ if (longPressTimerRef.current) {
113
+ clearTimeout(longPressTimerRef.current)
114
+ longPressTimerRef.current = null
115
+ }
116
+ }, [])
117
+
99
118
  // Clean up long press timer on unmount
100
119
  useEffect(() => {
101
120
  return () => {
@@ -106,15 +125,14 @@ export function Slot({
106
125
  const isHovered = hoveredSlot === index
107
126
  const isDragTarget = dragSlots.includes(index)
108
127
  const dragPreviewEntry = dragPreview.get(index)
109
- const isFocused = focusedSlot === index
110
- const showPKeyNumber = pKeyActive && index >= 0 && index <= 99
111
- const isInFocusSwapMode = focusedSlot !== null || pKeyActive
128
+ const isFocused = !disableFocusSwap && focusedSlot === index
129
+ const showPKeyNumber = !disableFocusSwap && pKeyActive && index >= 0 && index <= 99
112
130
 
113
- // Keyboard number key while hovering
131
+ // Keyboard number key while hovering (disabled on hotbar HUD)
114
132
  useEffect(() => {
115
- if (!isHovered || activeNumberKey === null || isMobile) return
133
+ if (!isHovered || activeNumberKey === null || isMobile || disableFocusSwap) return
116
134
  sendAction({ type: 'hotbar-swap', slotIndex: index, hotbarSlot: activeNumberKey })
117
- }, [activeNumberKey, isHovered, index, sendAction, isMobile])
135
+ }, [activeNumberKey, isHovered, index, sendAction, isMobile, disableFocusSwap])
118
136
 
119
137
  const handleMouseEnter = useCallback((e: React.MouseEvent) => {
120
138
  if (isMobile) return
@@ -136,7 +154,9 @@ export function Slot({
136
154
  dragEndedRef.current = false
137
155
  const button = e.button === 2 ? 'right' : e.button === 1 ? 'middle' : 'left'
138
156
  if (button === 'middle') {
139
- sendAction({ type: 'click', slotIndex: index, button: 'middle', mode: 'middle' })
157
+ if (!disableFocusSwap) {
158
+ sendAction({ type: 'click', slotIndex: index, button: 'middle', mode: 'middle' })
159
+ }
140
160
  return
141
161
  }
142
162
  if (heldItem && (button === 'left' || button === 'right')) {
@@ -145,7 +165,7 @@ export function Slot({
145
165
  startDrag(index, button)
146
166
  }
147
167
  },
148
- [isMobile, disabled, heldItem, index, sendAction, startDrag, dragEndedRef],
168
+ [isMobile, disabled, disableFocusSwap, heldItem, index, sendAction, startDrag, dragEndedRef],
149
169
  )
150
170
 
151
171
  const handleMouseUp = useCallback(
@@ -164,8 +184,8 @@ export function Slot({
164
184
  // without this guard they fall through to the click path below.
165
185
  if (dragEndedRef.current) return
166
186
 
167
- // Focus/swap logic — active in P mode OR when a slot is already focused
168
- if (button === 'left' && (pKeyActive || focusedSlot !== null)) {
187
+ // Focus/swap logic — active in P mode OR when a slot is already focused (disabled for hotbar HUD)
188
+ if (!disableFocusSwap && button === 'left' && (pKeyActive || focusedSlot !== null)) {
169
189
  if (pKeyActive) setPKeyActive(false)
170
190
  if (focusedSlot === null) {
171
191
  setFocusedSlot(index)
@@ -191,6 +211,21 @@ export function Slot({
191
211
  lastClickTimeRef.current = now
192
212
 
193
213
  const mode = e.shiftKey ? 'shift' : 'normal'
214
+
215
+ if (disableFocusSwap && !onClickOverride) {
216
+ if (button === 'middle') {
217
+ if (isDragging) endDrag()
218
+ return
219
+ }
220
+ if (!heldItem && index >= 36 && index <= 44) {
221
+ if (button === 'left' && mode === 'normal') {
222
+ sendAction({ type: 'hotbar-select', slotIndex: index })
223
+ }
224
+ if (isDragging) endDrag()
225
+ return
226
+ }
227
+ }
228
+
194
229
  if (onClickOverride) {
195
230
  onClickOverride(button, mode)
196
231
  } else {
@@ -203,7 +238,7 @@ export function Slot({
203
238
  }
204
239
  if (isDragging) endDrag()
205
240
  },
206
- [isMobile, disabled, isDragging, dragSlots.length, sendAction, index, endDrag, onClickOverride, resultSlot, heldItem, item, pKeyActive, setPKeyActive, focusedSlot, setFocusedSlot, dragEndedRef],
241
+ [isMobile, disabled, disableFocusSwap, isDragging, dragSlots.length, sendAction, index, endDrag, onClickOverride, resultSlot, heldItem, item, pKeyActive, setPKeyActive, focusedSlot, setFocusedSlot, dragEndedRef],
207
242
  )
208
243
 
209
244
  const handleDoubleClick = useCallback(
@@ -211,13 +246,14 @@ export function Slot({
211
246
  if (isMobile || disabled) return
212
247
  e.preventDefault()
213
248
  cancelDrag()
249
+ if (disableFocusSwap && !onClickOverride) return
214
250
  if (onClickOverride) {
215
251
  onClickOverride('left', 'double')
216
252
  } else {
217
253
  sendAction({ type: 'click', slotIndex: index, button: 'left', mode: 'double' })
218
254
  }
219
255
  },
220
- [isMobile, disabled, sendAction, index, onClickOverride, cancelDrag],
256
+ [isMobile, disabled, disableFocusSwap, sendAction, index, onClickOverride, cancelDrag],
221
257
  )
222
258
 
223
259
  const handleContextMenu = useCallback((e: React.MouseEvent) => {
@@ -226,7 +262,7 @@ export function Slot({
226
262
 
227
263
  const handleWheel = useCallback(
228
264
  (e: WheelEvent) => {
229
- if (isMobile || disabled) return
265
+ if (isMobile || disabled || disableFocusSwap) return
230
266
  if (onClickOverride) return // JEI slots: let parent handle wheel for pagination
231
267
  if (!item && !heldItem) return
232
268
  e.preventDefault()
@@ -236,7 +272,7 @@ export function Slot({
236
272
  sendAction({ type: 'click', slotIndex: index, button: 'right', mode: 'normal' })
237
273
  }
238
274
  },
239
- [isMobile, disabled, item, heldItem, sendAction, index, onClickOverride],
275
+ [isMobile, disabled, disableFocusSwap, item, heldItem, sendAction, index, onClickOverride],
240
276
  )
241
277
 
242
278
  // Attach wheel listener as non-passive so preventDefault() is effective.
@@ -249,18 +285,6 @@ export function Slot({
249
285
  return () => el.removeEventListener('wheel', handleWheel)
250
286
  }, [handleWheel])
251
287
 
252
- // Mobile touch handlers
253
- const touchStartRef = useRef<{ x: number; y: number; time: number } | null>(null)
254
- const longPressTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null)
255
- const longPressFiredRef = useRef(false)
256
-
257
- const cancelLongPress = useCallback(() => {
258
- if (longPressTimerRef.current) {
259
- clearTimeout(longPressTimerRef.current)
260
- longPressTimerRef.current = null
261
- }
262
- }, [])
263
-
264
288
  const handleTouchStart = useCallback(
265
289
  (e: React.TouchEvent) => {
266
290
  if (!isMobile) return
@@ -268,6 +292,18 @@ export function Slot({
268
292
  touchStartRef.current = { x: touch.clientX, y: touch.clientY, time: Date.now() }
269
293
  longPressFiredRef.current = false
270
294
  cancelLongPress()
295
+ // Hotbar HUD: staged long-press drops (no radial menu)
296
+ if (disableFocusSwap && item && !heldItem && !disabled) {
297
+ longPressTimerRef.current = setTimeout(() => {
298
+ longPressFiredRef.current = true
299
+ sendAction({ type: 'drop', slotIndex: index, all: false })
300
+ longPressTimerRef.current = setTimeout(() => {
301
+ sendAction({ type: 'drop', slotIndex: index, all: true })
302
+ longPressTimerRef.current = null
303
+ }, HOTBAR_LONG_PRESS_DROP_ALL_EXTRA_MS)
304
+ }, HOTBAR_LONG_PRESS_DROP_ONE_MS)
305
+ return
306
+ }
271
307
  // Long press: open mobile menu after 400ms if item exists and no held item
272
308
  if (item && !heldItem && !disabled) {
273
309
  const startX = touch.clientX
@@ -279,7 +315,7 @@ export function Slot({
279
315
  }, 400)
280
316
  }
281
317
  },
282
- [isMobile, item, heldItem, disabled, cancelLongPress],
318
+ [isMobile, item, heldItem, disabled, cancelLongPress, disableFocusSwap, sendAction, index],
283
319
  )
284
320
 
285
321
  const handleTouchMove = useCallback(
@@ -318,7 +354,7 @@ export function Slot({
318
354
  // Without this, the click bubbles to the inventory window div which clears focusedSlot.
319
355
  e.preventDefault()
320
356
 
321
- if (pKeyActive) setPKeyActive(false)
357
+ if (pKeyActive && !disableFocusSwap) setPKeyActive(false)
322
358
 
323
359
  // JEI / recipe / custom slots: same handler as desktop onMouseUp (not focus/swap).
324
360
  if (onClickOverride) {
@@ -333,6 +369,16 @@ export function Slot({
333
369
  return
334
370
  }
335
371
 
372
+ if (disableFocusSwap) {
373
+ if (focusedSlot !== null) setFocusedSlot(null)
374
+ if (index >= 36 && index <= 44 && !heldItem) {
375
+ sendAction({ type: 'hotbar-select', slotIndex: index })
376
+ } else {
377
+ sendAction({ type: 'click', slotIndex: index, button: 'left', mode: 'normal' })
378
+ }
379
+ return
380
+ }
381
+
336
382
  // On mobile, tapping always uses the focus/swap mechanism:
337
383
  // first tap focuses, second tap on a different slot swaps, same slot clears.
338
384
  if (focusedSlot === null) {
@@ -346,22 +392,22 @@ export function Slot({
346
392
  setFocusedSlot(null)
347
393
  }
348
394
  },
349
- [isMobile, disabled, heldItem, sendAction, index, pKeyActive, setPKeyActive, focusedSlot, setFocusedSlot, onClickOverride, cancelLongPress, mobileMenuOpen],
395
+ [isMobile, disabled, disableFocusSwap, heldItem, sendAction, index, pKeyActive, setPKeyActive, focusedSlot, setFocusedSlot, onClickOverride, cancelLongPress, mobileMenuOpen],
350
396
  )
351
397
 
352
398
  const handleMobilePickAll = useCallback(() => {
353
399
  setMobileMenuOpen(false)
354
400
  setShowTooltip(false)
355
- setFocusedSlot(index)
401
+ if (!disableFocusSwap) setFocusedSlot(index)
356
402
  sendAction({ type: 'click', slotIndex: index, button: 'left', mode: 'normal' })
357
- }, [sendAction, index, setFocusedSlot])
403
+ }, [sendAction, index, setFocusedSlot, disableFocusSwap])
358
404
 
359
405
  const handleMobilePickHalf = useCallback(() => {
360
406
  setMobileMenuOpen(false)
361
407
  setShowTooltip(false)
362
- setFocusedSlot(index)
408
+ if (!disableFocusSwap) setFocusedSlot(index)
363
409
  sendAction({ type: 'click', slotIndex: index, button: 'right', mode: 'normal' })
364
- }, [sendAction, index, setFocusedSlot])
410
+ }, [sendAction, index, setFocusedSlot, disableFocusSwap])
365
411
 
366
412
  const handleMobileDropOne = useCallback(() => {
367
413
  setMobileMenuOpen(false)
@@ -39,6 +39,8 @@ function describeAction(action: InventoryAction): string {
39
39
  return `Set beacon effects: ${action.primaryEffect} / ${action.secondaryEffect}`
40
40
  case 'hotbar-swap':
41
41
  return `Swap slot ${action.slotIndex} with hotbar ${action.hotbarSlot}`
42
+ case 'hotbar-select':
43
+ return `Select hotbar slot (index ${action.slotIndex})`
42
44
  default:
43
45
  return JSON.stringify(action)
44
46
  }
@@ -88,12 +90,34 @@ export function createDemoConnector(options: DemoConnectorOptions): InventoryCon
88
90
  if (actionLog.length > 100) actionLog.splice(100)
89
91
  options.onAction?.(entry)
90
92
 
93
+ if (action.type === 'hotbar-select') {
94
+ if (action.slotIndex >= 36 && action.slotIndex <= 44) {
95
+ playerState = { ...playerState, activeHotbarSlot: action.slotIndex - 36 }
96
+ emit({ type: 'playerUpdate', state: playerState })
97
+ }
98
+ return
99
+ }
100
+
91
101
  // Demo: simulate simple click behavior
92
102
  if (action.type === 'click' && windowState) {
93
103
  const slots = [...windowState.slots]
94
104
  const slotState = slots.find((s) => s.index === action.slotIndex)
95
105
  const held = windowState.heldItem
96
106
 
107
+ // Hotbar HUD: empty-hand click on main bar = select only (no pick)
108
+ if (
109
+ windowState.type === 'hotbar' &&
110
+ action.button === 'left' &&
111
+ action.mode === 'normal' &&
112
+ !held &&
113
+ action.slotIndex >= 36 &&
114
+ action.slotIndex <= 44
115
+ ) {
116
+ playerState = { ...playerState, activeHotbarSlot: action.slotIndex - 36 }
117
+ emit({ type: 'playerUpdate', state: playerState })
118
+ return
119
+ }
120
+
97
121
  if (action.button === 'left' && action.mode === 'normal') {
98
122
  if (held && slotState) {
99
123
  const idx = slots.indexOf(slotState)
@@ -603,6 +603,22 @@ export function createMineflayerConnector(bot: MineflayerBot, options?: Mineflay
603
603
  logActionEvent('connector.action.success', action)
604
604
  return
605
605
  }
606
+
607
+ if (action.type === 'hotbar-select') {
608
+ if (!hotbarOnly) {
609
+ logActionEvent('connector.action.skipped', action, { reason: 'not_hotbar_only' })
610
+ return
611
+ }
612
+ if (action.slotIndex < 36 || action.slotIndex > 44) {
613
+ logActionEvent('connector.action.skipped', action, { reason: 'invalid_hotbar_slot' })
614
+ return
615
+ }
616
+ const extBot = bot as MineflayerBot & { setQuickBarSlot?: (i: number) => void }
617
+ extBot.setQuickBarSlot?.(action.slotIndex - 36)
618
+ onSetSlot()
619
+ logActionEvent('connector.action.success', action)
620
+ return
621
+ }
606
622
 
607
623
  const win = bot.currentWindow
608
624
 
@@ -142,7 +142,7 @@ export function summarizeWindowState(state: InventoryWindowState | null | undefi
142
142
  }
143
143
 
144
144
  export function getActionSlotIndexes(action: InventoryAction): number[] | undefined {
145
- if (action.type === 'click' || action.type === 'drop' || action.type === 'hotbar-swap') return [action.slotIndex]
145
+ if (action.type === 'click' || action.type === 'drop' || action.type === 'hotbar-swap' || action.type === 'hotbar-select') return [action.slotIndex]
146
146
  if (action.type === 'drag') return [...action.slots]
147
147
  return undefined
148
148
  }
package/src/types.ts CHANGED
@@ -90,6 +90,8 @@ export type InventoryAction =
90
90
  | { type: 'enchant'; enchantIndex: number }
91
91
  | { type: 'beacon'; primaryEffect: number; secondaryEffect: number }
92
92
  | { type: 'hotbar-swap'; slotIndex: number; hotbarSlot: number }
93
+ /** Standalone hotbar HUD: change selected slot only (no window pick/swap). Slot index 36–44. */
94
+ | { type: 'hotbar-select'; slotIndex: number }
93
95
  /** Emitted by the Hotbar "open inventory" button; integrations (e.g. mineflayer) handle this. */
94
96
  | { type: 'open-inventory' }
95
97