minecraft-inventory 0.1.43 → 0.1.45
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 +3 -3
- package/src/components/RecipeGuide/RecipeInventoryView.tsx +55 -22
- package/src/components/Slot/Slot.module.css +2 -5
- package/src/components/Slot/Slot.tsx +93 -36
- package/src/components/Tooltip/ItemTooltipBody.tsx +69 -0
- package/src/components/Tooltip/Tooltip.tsx +2 -54
- 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
|
)}
|
|
@@ -47,8 +47,9 @@ export function InventoryWindow({
|
|
|
47
47
|
const { scale, slotSize, contentSize } = useScale()
|
|
48
48
|
// borderPx removed from slot positioning: registry coords already point to the item area
|
|
49
49
|
const borderPx = 0
|
|
50
|
+
const isHotbar = type === 'hotbar'
|
|
50
51
|
|
|
51
|
-
useKeyboardShortcuts(enableKeyboardShortcuts)
|
|
52
|
+
useKeyboardShortcuts(enableKeyboardShortcuts && !isHotbar)
|
|
52
53
|
|
|
53
54
|
const effectiveSlots = useMemo(() => {
|
|
54
55
|
if (slotsProp) return slotsProp
|
|
@@ -73,7 +74,6 @@ export function InventoryWindow({
|
|
|
73
74
|
const effectiveTitle = (type === 'player' || isBeacon) ? undefined : (title ?? windowState?.title ?? def.title)
|
|
74
75
|
const isVillager = type === 'villager'
|
|
75
76
|
const isEnchanting = def?.name === 'enchanting_table'
|
|
76
|
-
const isHotbar = type === 'hotbar'
|
|
77
77
|
const isAnvil = def?.name === 'anvil'
|
|
78
78
|
|
|
79
79
|
const resolveItem = (slotIndex: number) => {
|
|
@@ -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
|
}
|
|
@@ -4,9 +4,15 @@ import { useInventoryContext } from '../../context/InventoryContext'
|
|
|
4
4
|
import { useScale } from '../../context/ScaleContext'
|
|
5
5
|
import { ItemCanvas } from '../ItemCanvas'
|
|
6
6
|
import { Tooltip } from '../Tooltip'
|
|
7
|
+
import { ItemTooltipBody } from '../Tooltip/ItemTooltipBody'
|
|
8
|
+
import tooltipStyles from '../Tooltip/Tooltip.module.css'
|
|
7
9
|
import { useMobile } from '../../hooks/useMobile'
|
|
8
10
|
import styles from './Slot.module.css'
|
|
9
11
|
|
|
12
|
+
/** Hotbar HUD long-press: first threshold drops one item; hold longer for whole stack. */
|
|
13
|
+
const HOTBAR_LONG_PRESS_DROP_ONE_MS = 420
|
|
14
|
+
const HOTBAR_LONG_PRESS_DROP_ALL_EXTRA_MS = 600
|
|
15
|
+
|
|
10
16
|
interface SlotProps {
|
|
11
17
|
index: number
|
|
12
18
|
item: ItemStack | null
|
|
@@ -19,6 +25,8 @@ interface SlotProps {
|
|
|
19
25
|
style?: React.CSSProperties
|
|
20
26
|
/** Remove slot background/border (e.g. for JEI items) */
|
|
21
27
|
noBackground?: boolean
|
|
28
|
+
/** When true, skip P-key / focus-swap UI and mobile two-tap swap (e.g. standalone hotbar HUD). */
|
|
29
|
+
disableFocusSwap?: boolean
|
|
22
30
|
/** Override default click behavior - when provided, calls this instead of sendAction */
|
|
23
31
|
onClickOverride?: (button: 'left' | 'right' | 'middle', mode: 'normal' | 'shift' | 'double') => void
|
|
24
32
|
}
|
|
@@ -34,6 +42,7 @@ export function Slot({
|
|
|
34
42
|
className,
|
|
35
43
|
style,
|
|
36
44
|
noBackground,
|
|
45
|
+
disableFocusSwap = false,
|
|
37
46
|
onClickOverride,
|
|
38
47
|
}: SlotProps) {
|
|
39
48
|
const {
|
|
@@ -96,6 +105,18 @@ export function Slot({
|
|
|
96
105
|
}
|
|
97
106
|
}, [label, item, renderSize])
|
|
98
107
|
|
|
108
|
+
// Mobile touch — timer ref must exist before cleanup effect below
|
|
109
|
+
const touchStartRef = useRef<{ x: number; y: number; time: number } | null>(null)
|
|
110
|
+
const longPressTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null)
|
|
111
|
+
const longPressFiredRef = useRef(false)
|
|
112
|
+
|
|
113
|
+
const cancelLongPress = useCallback(() => {
|
|
114
|
+
if (longPressTimerRef.current) {
|
|
115
|
+
clearTimeout(longPressTimerRef.current)
|
|
116
|
+
longPressTimerRef.current = null
|
|
117
|
+
}
|
|
118
|
+
}, [])
|
|
119
|
+
|
|
99
120
|
// Clean up long press timer on unmount
|
|
100
121
|
useEffect(() => {
|
|
101
122
|
return () => {
|
|
@@ -106,15 +127,14 @@ export function Slot({
|
|
|
106
127
|
const isHovered = hoveredSlot === index
|
|
107
128
|
const isDragTarget = dragSlots.includes(index)
|
|
108
129
|
const dragPreviewEntry = dragPreview.get(index)
|
|
109
|
-
const isFocused = focusedSlot === index
|
|
110
|
-
const showPKeyNumber = pKeyActive && index >= 0 && index <= 99
|
|
111
|
-
const isInFocusSwapMode = focusedSlot !== null || pKeyActive
|
|
130
|
+
const isFocused = !disableFocusSwap && focusedSlot === index
|
|
131
|
+
const showPKeyNumber = !disableFocusSwap && pKeyActive && index >= 0 && index <= 99
|
|
112
132
|
|
|
113
|
-
// Keyboard number key while hovering
|
|
133
|
+
// Keyboard number key while hovering (disabled on hotbar HUD)
|
|
114
134
|
useEffect(() => {
|
|
115
|
-
if (!isHovered || activeNumberKey === null || isMobile) return
|
|
135
|
+
if (!isHovered || activeNumberKey === null || isMobile || disableFocusSwap) return
|
|
116
136
|
sendAction({ type: 'hotbar-swap', slotIndex: index, hotbarSlot: activeNumberKey })
|
|
117
|
-
}, [activeNumberKey, isHovered, index, sendAction, isMobile])
|
|
137
|
+
}, [activeNumberKey, isHovered, index, sendAction, isMobile, disableFocusSwap])
|
|
118
138
|
|
|
119
139
|
const handleMouseEnter = useCallback((e: React.MouseEvent) => {
|
|
120
140
|
if (isMobile) return
|
|
@@ -136,7 +156,9 @@ export function Slot({
|
|
|
136
156
|
dragEndedRef.current = false
|
|
137
157
|
const button = e.button === 2 ? 'right' : e.button === 1 ? 'middle' : 'left'
|
|
138
158
|
if (button === 'middle') {
|
|
139
|
-
|
|
159
|
+
if (!disableFocusSwap) {
|
|
160
|
+
sendAction({ type: 'click', slotIndex: index, button: 'middle', mode: 'middle' })
|
|
161
|
+
}
|
|
140
162
|
return
|
|
141
163
|
}
|
|
142
164
|
if (heldItem && (button === 'left' || button === 'right')) {
|
|
@@ -145,7 +167,7 @@ export function Slot({
|
|
|
145
167
|
startDrag(index, button)
|
|
146
168
|
}
|
|
147
169
|
},
|
|
148
|
-
[isMobile, disabled, heldItem, index, sendAction, startDrag, dragEndedRef],
|
|
170
|
+
[isMobile, disabled, disableFocusSwap, heldItem, index, sendAction, startDrag, dragEndedRef],
|
|
149
171
|
)
|
|
150
172
|
|
|
151
173
|
const handleMouseUp = useCallback(
|
|
@@ -164,8 +186,8 @@ export function Slot({
|
|
|
164
186
|
// without this guard they fall through to the click path below.
|
|
165
187
|
if (dragEndedRef.current) return
|
|
166
188
|
|
|
167
|
-
// Focus/swap logic — active in P mode OR when a slot is already focused
|
|
168
|
-
if (button === 'left' && (pKeyActive || focusedSlot !== null)) {
|
|
189
|
+
// Focus/swap logic — active in P mode OR when a slot is already focused (disabled for hotbar HUD)
|
|
190
|
+
if (!disableFocusSwap && button === 'left' && (pKeyActive || focusedSlot !== null)) {
|
|
169
191
|
if (pKeyActive) setPKeyActive(false)
|
|
170
192
|
if (focusedSlot === null) {
|
|
171
193
|
setFocusedSlot(index)
|
|
@@ -191,6 +213,21 @@ export function Slot({
|
|
|
191
213
|
lastClickTimeRef.current = now
|
|
192
214
|
|
|
193
215
|
const mode = e.shiftKey ? 'shift' : 'normal'
|
|
216
|
+
|
|
217
|
+
if (disableFocusSwap && !onClickOverride) {
|
|
218
|
+
if (button === 'middle') {
|
|
219
|
+
if (isDragging) endDrag()
|
|
220
|
+
return
|
|
221
|
+
}
|
|
222
|
+
if (!heldItem && index >= 36 && index <= 44) {
|
|
223
|
+
if (button === 'left' && mode === 'normal') {
|
|
224
|
+
sendAction({ type: 'hotbar-select', slotIndex: index })
|
|
225
|
+
}
|
|
226
|
+
if (isDragging) endDrag()
|
|
227
|
+
return
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
|
|
194
231
|
if (onClickOverride) {
|
|
195
232
|
onClickOverride(button, mode)
|
|
196
233
|
} else {
|
|
@@ -203,7 +240,7 @@ export function Slot({
|
|
|
203
240
|
}
|
|
204
241
|
if (isDragging) endDrag()
|
|
205
242
|
},
|
|
206
|
-
[isMobile, disabled, isDragging, dragSlots.length, sendAction, index, endDrag, onClickOverride, resultSlot, heldItem, item, pKeyActive, setPKeyActive, focusedSlot, setFocusedSlot, dragEndedRef],
|
|
243
|
+
[isMobile, disabled, disableFocusSwap, isDragging, dragSlots.length, sendAction, index, endDrag, onClickOverride, resultSlot, heldItem, item, pKeyActive, setPKeyActive, focusedSlot, setFocusedSlot, dragEndedRef],
|
|
207
244
|
)
|
|
208
245
|
|
|
209
246
|
const handleDoubleClick = useCallback(
|
|
@@ -211,13 +248,14 @@ export function Slot({
|
|
|
211
248
|
if (isMobile || disabled) return
|
|
212
249
|
e.preventDefault()
|
|
213
250
|
cancelDrag()
|
|
251
|
+
if (disableFocusSwap && !onClickOverride) return
|
|
214
252
|
if (onClickOverride) {
|
|
215
253
|
onClickOverride('left', 'double')
|
|
216
254
|
} else {
|
|
217
255
|
sendAction({ type: 'click', slotIndex: index, button: 'left', mode: 'double' })
|
|
218
256
|
}
|
|
219
257
|
},
|
|
220
|
-
[isMobile, disabled, sendAction, index, onClickOverride, cancelDrag],
|
|
258
|
+
[isMobile, disabled, disableFocusSwap, sendAction, index, onClickOverride, cancelDrag],
|
|
221
259
|
)
|
|
222
260
|
|
|
223
261
|
const handleContextMenu = useCallback((e: React.MouseEvent) => {
|
|
@@ -226,7 +264,7 @@ export function Slot({
|
|
|
226
264
|
|
|
227
265
|
const handleWheel = useCallback(
|
|
228
266
|
(e: WheelEvent) => {
|
|
229
|
-
if (isMobile || disabled) return
|
|
267
|
+
if (isMobile || disabled || disableFocusSwap) return
|
|
230
268
|
if (onClickOverride) return // JEI slots: let parent handle wheel for pagination
|
|
231
269
|
if (!item && !heldItem) return
|
|
232
270
|
e.preventDefault()
|
|
@@ -236,7 +274,7 @@ export function Slot({
|
|
|
236
274
|
sendAction({ type: 'click', slotIndex: index, button: 'right', mode: 'normal' })
|
|
237
275
|
}
|
|
238
276
|
},
|
|
239
|
-
[isMobile, disabled, item, heldItem, sendAction, index, onClickOverride],
|
|
277
|
+
[isMobile, disabled, disableFocusSwap, item, heldItem, sendAction, index, onClickOverride],
|
|
240
278
|
)
|
|
241
279
|
|
|
242
280
|
// Attach wheel listener as non-passive so preventDefault() is effective.
|
|
@@ -249,18 +287,6 @@ export function Slot({
|
|
|
249
287
|
return () => el.removeEventListener('wheel', handleWheel)
|
|
250
288
|
}, [handleWheel])
|
|
251
289
|
|
|
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
290
|
const handleTouchStart = useCallback(
|
|
265
291
|
(e: React.TouchEvent) => {
|
|
266
292
|
if (!isMobile) return
|
|
@@ -268,6 +294,18 @@ export function Slot({
|
|
|
268
294
|
touchStartRef.current = { x: touch.clientX, y: touch.clientY, time: Date.now() }
|
|
269
295
|
longPressFiredRef.current = false
|
|
270
296
|
cancelLongPress()
|
|
297
|
+
// Hotbar HUD: staged long-press drops (no radial menu)
|
|
298
|
+
if (disableFocusSwap && item && !heldItem && !disabled) {
|
|
299
|
+
longPressTimerRef.current = setTimeout(() => {
|
|
300
|
+
longPressFiredRef.current = true
|
|
301
|
+
sendAction({ type: 'drop', slotIndex: index, all: false })
|
|
302
|
+
longPressTimerRef.current = setTimeout(() => {
|
|
303
|
+
sendAction({ type: 'drop', slotIndex: index, all: true })
|
|
304
|
+
longPressTimerRef.current = null
|
|
305
|
+
}, HOTBAR_LONG_PRESS_DROP_ALL_EXTRA_MS)
|
|
306
|
+
}, HOTBAR_LONG_PRESS_DROP_ONE_MS)
|
|
307
|
+
return
|
|
308
|
+
}
|
|
271
309
|
// Long press: open mobile menu after 400ms if item exists and no held item
|
|
272
310
|
if (item && !heldItem && !disabled) {
|
|
273
311
|
const startX = touch.clientX
|
|
@@ -279,7 +317,7 @@ export function Slot({
|
|
|
279
317
|
}, 400)
|
|
280
318
|
}
|
|
281
319
|
},
|
|
282
|
-
[isMobile, item, heldItem, disabled, cancelLongPress],
|
|
320
|
+
[isMobile, item, heldItem, disabled, cancelLongPress, disableFocusSwap, sendAction, index],
|
|
283
321
|
)
|
|
284
322
|
|
|
285
323
|
const handleTouchMove = useCallback(
|
|
@@ -318,7 +356,7 @@ export function Slot({
|
|
|
318
356
|
// Without this, the click bubbles to the inventory window div which clears focusedSlot.
|
|
319
357
|
e.preventDefault()
|
|
320
358
|
|
|
321
|
-
if (pKeyActive) setPKeyActive(false)
|
|
359
|
+
if (pKeyActive && !disableFocusSwap) setPKeyActive(false)
|
|
322
360
|
|
|
323
361
|
// JEI / recipe / custom slots: same handler as desktop onMouseUp (not focus/swap).
|
|
324
362
|
if (onClickOverride) {
|
|
@@ -333,6 +371,16 @@ export function Slot({
|
|
|
333
371
|
return
|
|
334
372
|
}
|
|
335
373
|
|
|
374
|
+
if (disableFocusSwap) {
|
|
375
|
+
if (focusedSlot !== null) setFocusedSlot(null)
|
|
376
|
+
if (index >= 36 && index <= 44 && !heldItem) {
|
|
377
|
+
sendAction({ type: 'hotbar-select', slotIndex: index })
|
|
378
|
+
} else {
|
|
379
|
+
sendAction({ type: 'click', slotIndex: index, button: 'left', mode: 'normal' })
|
|
380
|
+
}
|
|
381
|
+
return
|
|
382
|
+
}
|
|
383
|
+
|
|
336
384
|
// On mobile, tapping always uses the focus/swap mechanism:
|
|
337
385
|
// first tap focuses, second tap on a different slot swaps, same slot clears.
|
|
338
386
|
if (focusedSlot === null) {
|
|
@@ -346,22 +394,22 @@ export function Slot({
|
|
|
346
394
|
setFocusedSlot(null)
|
|
347
395
|
}
|
|
348
396
|
},
|
|
349
|
-
[isMobile, disabled, heldItem, sendAction, index, pKeyActive, setPKeyActive, focusedSlot, setFocusedSlot, onClickOverride, cancelLongPress, mobileMenuOpen],
|
|
397
|
+
[isMobile, disabled, disableFocusSwap, heldItem, sendAction, index, pKeyActive, setPKeyActive, focusedSlot, setFocusedSlot, onClickOverride, cancelLongPress, mobileMenuOpen],
|
|
350
398
|
)
|
|
351
399
|
|
|
352
400
|
const handleMobilePickAll = useCallback(() => {
|
|
353
401
|
setMobileMenuOpen(false)
|
|
354
402
|
setShowTooltip(false)
|
|
355
|
-
setFocusedSlot(index)
|
|
403
|
+
if (!disableFocusSwap) setFocusedSlot(index)
|
|
356
404
|
sendAction({ type: 'click', slotIndex: index, button: 'left', mode: 'normal' })
|
|
357
|
-
}, [sendAction, index, setFocusedSlot])
|
|
405
|
+
}, [sendAction, index, setFocusedSlot, disableFocusSwap])
|
|
358
406
|
|
|
359
407
|
const handleMobilePickHalf = useCallback(() => {
|
|
360
408
|
setMobileMenuOpen(false)
|
|
361
409
|
setShowTooltip(false)
|
|
362
|
-
setFocusedSlot(index)
|
|
410
|
+
if (!disableFocusSwap) setFocusedSlot(index)
|
|
363
411
|
sendAction({ type: 'click', slotIndex: index, button: 'right', mode: 'normal' })
|
|
364
|
-
}, [sendAction, index, setFocusedSlot])
|
|
412
|
+
}, [sendAction, index, setFocusedSlot, disableFocusSwap])
|
|
365
413
|
|
|
366
414
|
const handleMobileDropOne = useCallback(() => {
|
|
367
415
|
setMobileMenuOpen(false)
|
|
@@ -560,7 +608,7 @@ function MobileSlotMenu({ item, x, y, onPickAll, onPickHalf, onDropOne, onDropAl
|
|
|
560
608
|
if (top + mh > vh - 4) top = vh - mh - 4
|
|
561
609
|
if (top < 4) top = 4
|
|
562
610
|
setPos({ left, top })
|
|
563
|
-
}, [x, y])
|
|
611
|
+
}, [x, y, item])
|
|
564
612
|
|
|
565
613
|
// Wrapper to handle both touch and click, preventing event bubbling to the slot
|
|
566
614
|
const touchBtn = (handler: () => void) => ({
|
|
@@ -585,8 +633,17 @@ function MobileSlotMenu({ item, x, y, onPickAll, onPickHalf, onDropOne, onDropAl
|
|
|
585
633
|
minWidth: 100 * scale,
|
|
586
634
|
}}
|
|
587
635
|
>
|
|
588
|
-
<div
|
|
589
|
-
{
|
|
636
|
+
<div
|
|
637
|
+
className={[tooltipStyles.tooltip, styles.mobileMenuInfo].join(' ')}
|
|
638
|
+
style={{
|
|
639
|
+
fontSize: Math.round(8 * scale),
|
|
640
|
+
padding: Math.round(3 * scale),
|
|
641
|
+
gap: Math.round(0.5 * scale),
|
|
642
|
+
width: 'max-content',
|
|
643
|
+
maxWidth: 'min(90vw, 280px)',
|
|
644
|
+
}}
|
|
645
|
+
>
|
|
646
|
+
<ItemTooltipBody item={item} />
|
|
590
647
|
</div>
|
|
591
648
|
<button className={styles.mobileBtn} {...touchBtn(onPickAll)}>Select All ({item.count})</button>
|
|
592
649
|
<button className={styles.mobileBtn} {...touchBtn(onPickHalf)}>Pick Half ({Math.ceil(item.count / 2)})</button>
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import React from 'react'
|
|
2
|
+
import type { ItemStack } from '../../types'
|
|
3
|
+
import { MessageFormattedString } from '../Text/MessageFormattedString'
|
|
4
|
+
import styles from './Tooltip.module.css'
|
|
5
|
+
|
|
6
|
+
function getRarityColor(item: ItemStack): string {
|
|
7
|
+
if (item.enchantments && item.enchantments.length > 0) return '#8080ff'
|
|
8
|
+
return '#ffffff'
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
function formatEnchantment(e: { name: string; level: number }): string {
|
|
12
|
+
const roman = ['', 'I', 'II', 'III', 'IV', 'V', 'VI', 'VII', 'VIII', 'IX', 'X']
|
|
13
|
+
return e.level <= 10 ? `${e.name} ${roman[e.level]}` : `${e.name} ${e.level}`
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export function getItemTooltipDisplayName(item: ItemStack): string {
|
|
17
|
+
return item.displayName ?? (item.name ? item.name.replace(/_/g, ' ') : `Item #${item.type}`)
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/** Item name, enchantments, lore, durability, and type id — shared by desktop tooltip and mobile menu. */
|
|
21
|
+
export function ItemTooltipBody({ item }: { item: ItemStack }) {
|
|
22
|
+
const nameColor = getRarityColor(item)
|
|
23
|
+
const hasDurability =
|
|
24
|
+
item.durability !== undefined &&
|
|
25
|
+
item.maxDurability !== undefined &&
|
|
26
|
+
item.durability < item.maxDurability
|
|
27
|
+
|
|
28
|
+
return (
|
|
29
|
+
<>
|
|
30
|
+
<div className={styles.name} style={{ color: nameColor }}>
|
|
31
|
+
<MessageFormattedString
|
|
32
|
+
text={getItemTooltipDisplayName(item)}
|
|
33
|
+
fallbackColor={nameColor}
|
|
34
|
+
/>
|
|
35
|
+
</div>
|
|
36
|
+
|
|
37
|
+
{item.enchantments && item.enchantments.length > 0 && (
|
|
38
|
+
<div className={styles.section}>
|
|
39
|
+
{item.enchantments.map((e, i) => (
|
|
40
|
+
<div key={i} className={styles.enchantment}>
|
|
41
|
+
{formatEnchantment(e)}
|
|
42
|
+
</div>
|
|
43
|
+
))}
|
|
44
|
+
</div>
|
|
45
|
+
)}
|
|
46
|
+
|
|
47
|
+
{item.lore && item.lore.length > 0 && (
|
|
48
|
+
<div className={styles.lore}>
|
|
49
|
+
{item.lore.map((line, i) => (
|
|
50
|
+
<div key={i}>
|
|
51
|
+
<MessageFormattedString text={line} />
|
|
52
|
+
</div>
|
|
53
|
+
))}
|
|
54
|
+
</div>
|
|
55
|
+
)}
|
|
56
|
+
|
|
57
|
+
{hasDurability && (
|
|
58
|
+
<div className={styles.durability}>
|
|
59
|
+
Durability: {item.durability} / {item.maxDurability}
|
|
60
|
+
</div>
|
|
61
|
+
)}
|
|
62
|
+
|
|
63
|
+
<div className={styles.typeId}>
|
|
64
|
+
{item.name ?? `#${item.type}`}
|
|
65
|
+
{item.metadata !== undefined && item.metadata > 0 ? `:${item.metadata}` : ''}
|
|
66
|
+
</div>
|
|
67
|
+
</>
|
|
68
|
+
)
|
|
69
|
+
}
|
|
@@ -2,8 +2,8 @@ import React, { useRef, useEffect } from 'react'
|
|
|
2
2
|
import { createPortal } from 'react-dom'
|
|
3
3
|
import type { ItemStack } from '../../types'
|
|
4
4
|
import { useScale } from '../../context/ScaleContext'
|
|
5
|
-
import { MessageFormattedString } from '../Text/MessageFormattedString'
|
|
6
5
|
import { globalMouse } from '../../utils/globalMouse'
|
|
6
|
+
import { ItemTooltipBody } from './ItemTooltipBody'
|
|
7
7
|
import styles from './Tooltip.module.css'
|
|
8
8
|
|
|
9
9
|
interface TooltipProps {
|
|
@@ -11,16 +11,6 @@ interface TooltipProps {
|
|
|
11
11
|
visible: boolean
|
|
12
12
|
}
|
|
13
13
|
|
|
14
|
-
function getRarityColor(item: ItemStack): string {
|
|
15
|
-
if (item.enchantments && item.enchantments.length > 0) return '#8080ff'
|
|
16
|
-
return '#ffffff'
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
function formatEnchantment(e: { name: string; level: number }): string {
|
|
20
|
-
const roman = ['', 'I', 'II', 'III', 'IV', 'V', 'VI', 'VII', 'VIII', 'IX', 'X']
|
|
21
|
-
return e.level <= 10 ? `${e.name} ${roman[e.level]}` : `${e.name} ${e.level}`
|
|
22
|
-
}
|
|
23
|
-
|
|
24
14
|
export function Tooltip({ item, visible }: TooltipProps) {
|
|
25
15
|
const { scale } = useScale()
|
|
26
16
|
const ref = useRef<HTMLDivElement>(null)
|
|
@@ -63,12 +53,6 @@ export function Tooltip({ item, visible }: TooltipProps) {
|
|
|
63
53
|
|
|
64
54
|
if (!visible) return null
|
|
65
55
|
|
|
66
|
-
const nameColor = getRarityColor(item)
|
|
67
|
-
const hasDurability =
|
|
68
|
-
item.durability !== undefined &&
|
|
69
|
-
item.maxDurability !== undefined &&
|
|
70
|
-
item.durability < item.maxDurability
|
|
71
|
-
|
|
72
56
|
const fs = Math.round(8 * scale)
|
|
73
57
|
const pad = Math.round(3 * scale)
|
|
74
58
|
const gap2 = Math.round(0.5 * scale)
|
|
@@ -91,43 +75,7 @@ export function Tooltip({ item, visible }: TooltipProps) {
|
|
|
91
75
|
pointerEvents: 'none',
|
|
92
76
|
}}
|
|
93
77
|
>
|
|
94
|
-
<
|
|
95
|
-
<MessageFormattedString
|
|
96
|
-
text={item.displayName ?? (item.name ? item.name.replace(/_/g, ' ') : `Item #${item.type}`)}
|
|
97
|
-
fallbackColor={nameColor}
|
|
98
|
-
/>
|
|
99
|
-
</div>
|
|
100
|
-
|
|
101
|
-
{item.enchantments && item.enchantments.length > 0 && (
|
|
102
|
-
<div className={styles.section}>
|
|
103
|
-
{item.enchantments.map((e, i) => (
|
|
104
|
-
<div key={i} className={styles.enchantment}>
|
|
105
|
-
{formatEnchantment(e)}
|
|
106
|
-
</div>
|
|
107
|
-
))}
|
|
108
|
-
</div>
|
|
109
|
-
)}
|
|
110
|
-
|
|
111
|
-
{item.lore && item.lore.length > 0 && (
|
|
112
|
-
<div className={styles.lore}>
|
|
113
|
-
{item.lore.map((line, i) => (
|
|
114
|
-
<div key={i}>
|
|
115
|
-
<MessageFormattedString text={line} />
|
|
116
|
-
</div>
|
|
117
|
-
))}
|
|
118
|
-
</div>
|
|
119
|
-
)}
|
|
120
|
-
|
|
121
|
-
{hasDurability && (
|
|
122
|
-
<div className={styles.durability}>
|
|
123
|
-
Durability: {item.durability} / {item.maxDurability}
|
|
124
|
-
</div>
|
|
125
|
-
)}
|
|
126
|
-
|
|
127
|
-
<div className={styles.typeId}>
|
|
128
|
-
{item.name ?? `#${item.type}`}
|
|
129
|
-
{item.metadata !== undefined && item.metadata > 0 ? `:${item.metadata}` : ''}
|
|
130
|
-
</div>
|
|
78
|
+
<ItemTooltipBody item={item} />
|
|
131
79
|
</div>
|
|
132
80
|
)
|
|
133
81
|
|
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
|
|