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.
- package/package.json +2 -2
- package/src/assets/entities/horse.png +0 -0
- package/src/assets/entities/llama.png +0 -0
- package/src/assets/entities/player.png +0 -0
- package/src/components/InventoryOverlay/InventoryOverlay.tsx +56 -9
- package/src/components/InventoryWindow/AnvilInput.tsx +102 -0
- package/src/components/InventoryWindow/EntityDisplay.tsx +46 -0
- package/src/components/InventoryWindow/InventoryBackground.tsx +72 -31
- package/src/components/InventoryWindow/InventoryWindow.tsx +14 -0
- package/src/components/ItemCanvas/ItemCanvas.tsx +10 -7
- package/src/components/JEI/JEI.tsx +7 -1
- package/src/components/Notes/Notes.tsx +76 -45
- package/src/components/Slot/Slot.tsx +114 -11
- package/src/connector/demo.ts +65 -0
- package/src/connector/mineflayer.ts +83 -25
- package/src/context/InventoryContext.tsx +160 -9
- package/src/context/TextureContext.tsx +9 -2
- package/src/generated/localTextures.ts +24 -15
- package/src/hooks/useKeyboardShortcuts.ts +61 -6
- package/src/index.tsx +5 -1
- package/src/registry/index.ts +30 -1
- package/src/registry/inventories.ts +99 -6
- package/src/styles/tokens.css +6 -0
- package/src/types.ts +30 -0
- package/src/utils/isItemEqual.ts +41 -0
|
@@ -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
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
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
|
-
|
|
142
|
-
|
|
143
|
-
|
|
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
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
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,
|
|
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
|
-
{
|
|
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
|
|
package/src/connector/demo.ts
CHANGED
|
@@ -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
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
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(
|
|
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:
|
|
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 (
|
|
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:
|
|
83
|
-
type:
|
|
84
|
-
title:
|
|
85
|
-
slots:
|
|
86
|
-
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
234
|
+
heldItem: convert(bot.heldItem),
|
|
177
235
|
}
|
|
178
236
|
|
|
179
237
|
emit({ type: 'windowOpen', state: windowState })
|