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 +1 -1
- package/src/components/InventoryOverlay/InventoryOverlay.tsx +1 -1
- package/src/components/InventoryWindow/HotbarExtras.tsx +1 -1
- package/src/components/InventoryWindow/InventoryWindow.tsx +1 -1
- package/src/components/RecipeGuide/RecipeInventoryView.tsx +55 -22
- package/src/components/Slot/Slot.tsx +79 -33
- package/src/connector/demo.ts +24 -0
- package/src/connector/mineflayer.ts +16 -0
- package/src/debug/inventoryDebug.ts +1 -1
- package/src/types.ts +2 -0
package/package.json
CHANGED
|
@@ -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
|
|
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
|
-
|
|
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)
|
package/src/connector/demo.ts
CHANGED
|
@@ -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
|
|