minecraft-inventory 0.1.4 → 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 +1 -1
- package/src/components/InventoryWindow/InventoryBackground.tsx +69 -82
- package/src/components/Notes/Notes.tsx +76 -45
- package/src/components/Slot/Slot.tsx +6 -0
- package/src/connector/demo.ts +65 -0
- package/src/connector/mineflayer.ts +3 -0
- package/src/context/InventoryContext.tsx +34 -17
- package/src/index.tsx +1 -1
- package/src/registry/index.ts +30 -1
- package/src/registry/inventories.ts +10 -9
- package/src/types.ts +6 -0
package/package.json
CHANGED
|
@@ -1,54 +1,24 @@
|
|
|
1
|
-
import React
|
|
1
|
+
import React from 'react'
|
|
2
2
|
import { useTextures } from '../../context/TextureContext'
|
|
3
3
|
import { useScale } from '../../context/ScaleContext'
|
|
4
4
|
import { MessageFormattedString } from '../Text/MessageFormattedString'
|
|
5
5
|
import type { InventoryTypeDefinition } from '../../registry'
|
|
6
6
|
|
|
7
7
|
/**
|
|
8
|
-
* For generic_9xN (N < 6):
|
|
9
|
-
*
|
|
10
|
-
*
|
|
8
|
+
* For generic_9xN (N < 6): CSS-stitch the 6-row generic_54 texture using two
|
|
9
|
+
* overlapping img elements with objectPosition to clip top and bottom portions.
|
|
10
|
+
* No canvas or async loading needed — works with any URL including remote ones.
|
|
11
11
|
*
|
|
12
|
-
* The source texture (generic_54.png, 176×222) layout:
|
|
13
|
-
* y=0..16
|
|
14
|
-
* y=17..124 — 6 container rows (6×18 = 108px)
|
|
15
|
-
* y=
|
|
12
|
+
* The source texture (generic_54.png, 176×222) fixed layout:
|
|
13
|
+
* y=0 ..16 — title bar (17px)
|
|
14
|
+
* y=17 ..124 — 6 container rows (6×18 = 108px)
|
|
15
|
+
* y=125 ..221 — player inventory section (97px)
|
|
16
|
+
*
|
|
17
|
+
* Output height = topH + playerH = N*18+17 + 97 = N*18+114.
|
|
18
|
+
* Registry backgroundHeight must equal N*18+114 for the div to match.
|
|
16
19
|
*/
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
useEffect(() => {
|
|
21
|
-
if (containerRows >= 6) {
|
|
22
|
-
setDataUrl(srcUrl)
|
|
23
|
-
return
|
|
24
|
-
}
|
|
25
|
-
let cancelled = false
|
|
26
|
-
const img = new window.Image()
|
|
27
|
-
img.crossOrigin = 'anonymous'
|
|
28
|
-
img.onload = () => {
|
|
29
|
-
if (cancelled) return
|
|
30
|
-
const srcW = 176
|
|
31
|
-
const srcH = img.naturalHeight // 222 for generic_54
|
|
32
|
-
const topH = containerRows * 18 + 17
|
|
33
|
-
const playerH = 96
|
|
34
|
-
const canvas = document.createElement('canvas')
|
|
35
|
-
canvas.width = srcW
|
|
36
|
-
canvas.height = topH + playerH
|
|
37
|
-
const ctx = canvas.getContext('2d')
|
|
38
|
-
if (!ctx) { setDataUrl(srcUrl); return }
|
|
39
|
-
// Top: title + N container rows
|
|
40
|
-
ctx.drawImage(img, 0, 0, srcW, topH, 0, 0, srcW, topH)
|
|
41
|
-
// Bottom: player inventory section (last 96px of source)
|
|
42
|
-
ctx.drawImage(img, 0, srcH - playerH, srcW, playerH, 0, topH, srcW, playerH)
|
|
43
|
-
setDataUrl(canvas.toDataURL())
|
|
44
|
-
}
|
|
45
|
-
img.onerror = () => { if (!cancelled) setDataUrl(srcUrl) }
|
|
46
|
-
img.src = srcUrl
|
|
47
|
-
return () => { cancelled = true }
|
|
48
|
-
}, [srcUrl, containerRows])
|
|
49
|
-
|
|
50
|
-
return dataUrl
|
|
51
|
-
}
|
|
20
|
+
const SRC_PLAYER_Y = 17 + 6 * 18 // 125 — fixed position where player section starts
|
|
21
|
+
const PLAYER_H = 222 - SRC_PLAYER_Y // 97px — height of player section in generic_54.png
|
|
52
22
|
|
|
53
23
|
interface InventoryBackgroundProps {
|
|
54
24
|
definition: InventoryTypeDefinition
|
|
@@ -66,20 +36,24 @@ export function InventoryBackground({
|
|
|
66
36
|
const textures = useTextures()
|
|
67
37
|
const { scale } = useScale()
|
|
68
38
|
|
|
69
|
-
const
|
|
70
|
-
|
|
71
|
-
const stitchedUrl = useStitchedTexture(rawBgUrl, definition.containerRows ?? 6)
|
|
72
|
-
const bgUrl = definition.containerRows != null && definition.containerRows < 6
|
|
73
|
-
? stitchedUrl // may be null while stitching
|
|
74
|
-
: rawBgUrl
|
|
39
|
+
const bgUrl = textures.getGuiTextureUrl(definition.backgroundTexture)
|
|
40
|
+
const isStitched = definition.containerRows != null && definition.containerRows < 6
|
|
75
41
|
|
|
76
42
|
const w = definition.backgroundWidth * scale
|
|
77
43
|
const h = definition.backgroundHeight * scale
|
|
78
44
|
|
|
79
|
-
// Source dimensions from definition (e.g., 176x166) — clip to this region from texture
|
|
80
45
|
const srcW = definition.backgroundWidth
|
|
81
46
|
const srcH = definition.backgroundHeight
|
|
82
47
|
|
|
48
|
+
const sharedImgStyle: React.CSSProperties = {
|
|
49
|
+
display: 'block',
|
|
50
|
+
width: srcW,
|
|
51
|
+
imageRendering: 'pixelated',
|
|
52
|
+
pointerEvents: 'none',
|
|
53
|
+
userSelect: 'none',
|
|
54
|
+
objectFit: 'none',
|
|
55
|
+
}
|
|
56
|
+
|
|
83
57
|
return (
|
|
84
58
|
<div
|
|
85
59
|
className="mc-inv-background"
|
|
@@ -93,42 +67,55 @@ export function InventoryBackground({
|
|
|
93
67
|
outlineOffset: 0,
|
|
94
68
|
}}
|
|
95
69
|
>
|
|
96
|
-
{
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
top: 0,
|
|
102
|
-
left: 0,
|
|
103
|
-
width: srcW,
|
|
104
|
-
height: srcH,
|
|
105
|
-
overflow: 'hidden',
|
|
106
|
-
transform: `scale(${scale})`,
|
|
107
|
-
transformOrigin: 'top left',
|
|
108
|
-
}}
|
|
109
|
-
>
|
|
110
|
-
{/* Background texture — render at natural size, clipped by wrapper overflow */}
|
|
111
|
-
{bgUrl && (
|
|
112
|
-
<img
|
|
113
|
-
className="mc-inv-background-image"
|
|
114
|
-
src={bgUrl}
|
|
115
|
-
alt=""
|
|
116
|
-
aria-hidden
|
|
70
|
+
{isStitched ? (
|
|
71
|
+
/* CSS two-part stitch: clips top (title+N rows) and bottom (player section)
|
|
72
|
+
from the same source image using objectPosition — no canvas/async needed. */
|
|
73
|
+
<div
|
|
74
|
+
className="mc-inv-background-wrapper"
|
|
117
75
|
style={{
|
|
118
|
-
|
|
76
|
+
position: 'absolute',
|
|
77
|
+
top: 0,
|
|
78
|
+
left: 0,
|
|
79
|
+
transform: `scale(${scale})`,
|
|
80
|
+
transformOrigin: 'top left',
|
|
81
|
+
}}
|
|
82
|
+
>
|
|
83
|
+
{/* Top: title bar + N container rows */}
|
|
84
|
+
<div style={{ width: srcW, height: definition.containerRows! * 18 + 17, overflow: 'hidden' }}>
|
|
85
|
+
<img className="mc-inv-background-image" src={bgUrl} alt="" aria-hidden draggable={false}
|
|
86
|
+
style={{ ...sharedImgStyle, objectPosition: '0 0' }} />
|
|
87
|
+
</div>
|
|
88
|
+
{/* Bottom: player inventory section starting at SRC_PLAYER_Y in source */}
|
|
89
|
+
<div style={{ width: srcW, height: PLAYER_H, overflow: 'hidden' }}>
|
|
90
|
+
<img src={bgUrl} alt="" aria-hidden draggable={false}
|
|
91
|
+
style={{ ...sharedImgStyle, objectPosition: `0 -${SRC_PLAYER_Y}px` }} />
|
|
92
|
+
</div>
|
|
93
|
+
</div>
|
|
94
|
+
) : (
|
|
95
|
+
/* Standard: clip source to srcW×srcH via overflow:hidden, then scale */
|
|
96
|
+
<div
|
|
97
|
+
className="mc-inv-background-wrapper"
|
|
98
|
+
style={{
|
|
99
|
+
position: 'absolute',
|
|
100
|
+
top: 0,
|
|
101
|
+
left: 0,
|
|
119
102
|
width: srcW,
|
|
120
103
|
height: srcH,
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
// Clip to top-left srcW×srcH region (if texture is larger)
|
|
125
|
-
objectFit: 'none',
|
|
126
|
-
objectPosition: '0 0',
|
|
104
|
+
overflow: 'hidden',
|
|
105
|
+
transform: `scale(${scale})`,
|
|
106
|
+
transformOrigin: 'top left',
|
|
127
107
|
}}
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
108
|
+
>
|
|
109
|
+
<img
|
|
110
|
+
className="mc-inv-background-image"
|
|
111
|
+
src={bgUrl}
|
|
112
|
+
alt=""
|
|
113
|
+
aria-hidden
|
|
114
|
+
draggable={false}
|
|
115
|
+
style={{ ...sharedImgStyle, objectPosition: '0 0' }}
|
|
116
|
+
/>
|
|
117
|
+
</div>
|
|
118
|
+
)}
|
|
132
119
|
|
|
133
120
|
{/* Title */}
|
|
134
121
|
{title !== undefined && (
|
|
@@ -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
|
)
|
|
@@ -244,6 +244,9 @@ export function Slot({
|
|
|
244
244
|
const touch = e.changedTouches[0]
|
|
245
245
|
if (Math.abs(touch.clientX - start.x) > 10 || Math.abs(touch.clientY - start.y) > 10) return
|
|
246
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()
|
|
247
250
|
|
|
248
251
|
if (pKeyActive) setPKeyActive(false)
|
|
249
252
|
|
|
@@ -319,6 +322,9 @@ export function Slot({
|
|
|
319
322
|
.filter(Boolean)
|
|
320
323
|
.join(' ')}
|
|
321
324
|
tabIndex={index >= 0 ? 0 : undefined}
|
|
325
|
+
data-slot={index}
|
|
326
|
+
data-debug={item?.debugKey ?? undefined}
|
|
327
|
+
data-texture={item?.textureKey ?? undefined}
|
|
322
328
|
style={{
|
|
323
329
|
width: renderSize,
|
|
324
330
|
height: renderSize,
|
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)
|
|
@@ -35,6 +35,9 @@ function makeSlotConverter(itemMapper?: MineflayerConnectorOptions['itemMapper']
|
|
|
35
35
|
count: slot.count,
|
|
36
36
|
metadata: slot.metadata,
|
|
37
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),
|
|
38
41
|
}
|
|
39
42
|
return itemMapper ? itemMapper(slot, mapped) : mapped
|
|
40
43
|
}
|
|
@@ -137,12 +137,11 @@ export function InventoryProvider({ connector, children, noDragSpread = false }:
|
|
|
137
137
|
})
|
|
138
138
|
if (compatibleSlots.length === 0) return preview
|
|
139
139
|
const perSlot = Math.floor(held.count / compatibleSlots.length)
|
|
140
|
-
|
|
140
|
+
// Vanilla behavior: if perSlot=0 (more slots than items), nothing is distributed
|
|
141
|
+
if (perSlot === 0) return preview
|
|
141
142
|
for (const idx of compatibleSlots) {
|
|
142
143
|
const existingCount = ws?.slots.find((s) => s.index === idx)?.item?.count ?? 0
|
|
143
|
-
const
|
|
144
|
-
if (remainder > 0) remainder--
|
|
145
|
-
const total = Math.min(existingCount + add, maxStack)
|
|
144
|
+
const total = Math.min(existingCount + perSlot, maxStack)
|
|
146
145
|
preview.set(idx, { count: total })
|
|
147
146
|
}
|
|
148
147
|
} else {
|
|
@@ -197,21 +196,26 @@ export function InventoryProvider({ connector, children, noDragSpread = false }:
|
|
|
197
196
|
})
|
|
198
197
|
if (compatibleSlots.length > 0) {
|
|
199
198
|
const perSlot = Math.floor(held.count / compatibleSlots.length)
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
const
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
199
|
+
// Vanilla behavior: if perSlot=0 (more slots than items), nothing is distributed
|
|
200
|
+
if (perSlot > 0) {
|
|
201
|
+
let totalPlaced = 0
|
|
202
|
+
for (const idx of compatibleSlots) {
|
|
203
|
+
const existingIdx = newSlots.findIndex((s) => s.index === idx)
|
|
204
|
+
const existingCount = existingIdx >= 0 ? (newSlots[existingIdx].item?.count ?? 0) : 0
|
|
205
|
+
const add = Math.min(perSlot, maxStack - existingCount)
|
|
206
|
+
totalPlaced += add
|
|
207
|
+
const newCount = existingCount + add
|
|
208
|
+
if (existingIdx >= 0) {
|
|
209
|
+
newSlots[existingIdx] = { index: idx, item: { ...held, count: newCount } }
|
|
210
|
+
} else {
|
|
211
|
+
newSlots.push({ index: idx, item: { ...held, count: newCount } })
|
|
212
|
+
}
|
|
211
213
|
}
|
|
214
|
+
if (ws) setWindowState({ ...ws, slots: newSlots })
|
|
215
|
+
const remaining = held.count - totalPlaced
|
|
216
|
+
if (remaining > 0) setHeldItemState({ ...held, count: remaining })
|
|
217
|
+
else setHeldItemState(null)
|
|
212
218
|
}
|
|
213
|
-
if (ws) setWindowState({ ...ws, slots: newSlots })
|
|
214
|
-
setHeldItemState(null)
|
|
215
219
|
}
|
|
216
220
|
} else {
|
|
217
221
|
// Right-click drag: place 1 per slot
|
|
@@ -256,6 +260,19 @@ export function InventoryProvider({ connector, children, noDragSpread = false }:
|
|
|
256
260
|
[windowState],
|
|
257
261
|
)
|
|
258
262
|
|
|
263
|
+
// Expose state to window for easier debugging from browser DevTools.
|
|
264
|
+
// Access via: window.__mcInv
|
|
265
|
+
useEffect(() => {
|
|
266
|
+
;(window as unknown as Record<string, unknown>).__mcInv = {
|
|
267
|
+
get windowState() { return windowStateRef.current },
|
|
268
|
+
get heldItem() { return heldItemRef.current },
|
|
269
|
+
get dragSlots() { return dragSlots },
|
|
270
|
+
get isDragging() { return isDragging },
|
|
271
|
+
get focusedSlot() { return focusedSlot },
|
|
272
|
+
get pKeyActive() { return pKeyActive },
|
|
273
|
+
}
|
|
274
|
+
})
|
|
275
|
+
|
|
259
276
|
const value: InventoryContextValue = {
|
|
260
277
|
windowState,
|
|
261
278
|
playerState,
|
package/src/index.tsx
CHANGED
|
@@ -49,7 +49,7 @@ export type {
|
|
|
49
49
|
export type { ActionLogEntry, DemoConnectorOptions } from './connector/demo'
|
|
50
50
|
|
|
51
51
|
// Registry
|
|
52
|
-
export { registerInventoryType, getInventoryType, getAllInventoryTypes } from './registry'
|
|
52
|
+
export { registerInventoryType, registerTypeAlias, getInventoryType, getAllInventoryTypes } from './registry'
|
|
53
53
|
export type { InventoryTypeDefinition, WindowType } from './registry'
|
|
54
54
|
|
|
55
55
|
// Types
|
package/src/registry/index.ts
CHANGED
|
@@ -5,12 +5,41 @@ const registry = new Map<string, InventoryTypeDefinition>(
|
|
|
5
5
|
Object.entries(inventoryDefinitions),
|
|
6
6
|
)
|
|
7
7
|
|
|
8
|
+
/**
|
|
9
|
+
* Maps alternative names/aliases to canonical inventory type names.
|
|
10
|
+
* Both the alias and the canonical name resolve to the same definition.
|
|
11
|
+
*
|
|
12
|
+
* Add entries here to support shorthand or legacy type identifiers.
|
|
13
|
+
*/
|
|
14
|
+
const typeAliases: Record<string, string> = {
|
|
15
|
+
// Shorthand aliases
|
|
16
|
+
crafting3x3: 'crafting_table',
|
|
17
|
+
crafting: 'crafting_table',
|
|
18
|
+
chest: 'chest', // already canonical, listed for clarity
|
|
19
|
+
// Protocol-level minecraft: prefix stripping is handled in getInventoryType
|
|
20
|
+
}
|
|
21
|
+
|
|
8
22
|
export function registerInventoryType(def: InventoryTypeDefinition): void {
|
|
9
23
|
registry.set(def.name, def)
|
|
10
24
|
}
|
|
11
25
|
|
|
26
|
+
/**
|
|
27
|
+
* Resolve an inventory type name or alias to its definition.
|
|
28
|
+
* Handles:
|
|
29
|
+
* - Exact matches (e.g. "crafting_table")
|
|
30
|
+
* - Aliases defined in `typeAliases` (e.g. "crafting3x3" → "crafting_table")
|
|
31
|
+
* - "minecraft:" namespace prefix (e.g. "minecraft:generic_9x3" → "generic_9x3")
|
|
32
|
+
*/
|
|
12
33
|
export function getInventoryType(name: string): InventoryTypeDefinition | undefined {
|
|
13
|
-
|
|
34
|
+
// Strip "minecraft:" namespace prefix if present
|
|
35
|
+
const stripped = name.startsWith('minecraft:') ? name.slice('minecraft:'.length) : name
|
|
36
|
+
// Resolve alias if defined
|
|
37
|
+
const canonical = typeAliases[stripped] ?? stripped
|
|
38
|
+
return registry.get(canonical)
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export function registerTypeAlias(alias: string, canonical: string): void {
|
|
42
|
+
typeAliases[alias] = canonical
|
|
14
43
|
}
|
|
15
44
|
|
|
16
45
|
export function getAllInventoryTypes(): InventoryTypeDefinition[] {
|
|
@@ -127,15 +127,16 @@ export const inventoryDefinitions = makeInventoryDefinitions({
|
|
|
127
127
|
|
|
128
128
|
// generic_9x1..9x6 all use generic_54.png (the 6-row chest texture, 176×222).
|
|
129
129
|
// InventoryBackground canvas-stitches rows < 6: takes the top N rows from the source
|
|
130
|
-
// then the player-inventory section
|
|
131
|
-
//
|
|
132
|
-
//
|
|
130
|
+
// (y=0..topH-1, where topH = N*18+17) then the player-inventory section starting at
|
|
131
|
+
// y=SRC_PLAYER_Y=125 and composites them.
|
|
132
|
+
// Output height formula: N*18+17 + (222-125) = N*18+17+97 = N*18+114.
|
|
133
|
+
// Slot formula: container at y=18, player at y = N*18+30 (13px frame into player section).
|
|
133
134
|
generic_9x1: {
|
|
134
135
|
name: 'generic_9x1',
|
|
135
136
|
title: 'Container',
|
|
136
137
|
backgroundTexture: '1.21.11/textures/gui/container/generic_54.png',
|
|
137
138
|
backgroundWidth: 176,
|
|
138
|
-
backgroundHeight:
|
|
139
|
+
backgroundHeight: 132, // 1*18 + 114
|
|
139
140
|
containerRows: 1,
|
|
140
141
|
slots: [
|
|
141
142
|
...gridSlots(9, 1, 8, 18, 'container'),
|
|
@@ -148,7 +149,7 @@ export const inventoryDefinitions = makeInventoryDefinitions({
|
|
|
148
149
|
title: 'Container',
|
|
149
150
|
backgroundTexture: '1.21.11/textures/gui/container/generic_54.png',
|
|
150
151
|
backgroundWidth: 176,
|
|
151
|
-
backgroundHeight:
|
|
152
|
+
backgroundHeight: 150, // 2*18 + 114
|
|
152
153
|
containerRows: 2,
|
|
153
154
|
slots: [
|
|
154
155
|
...gridSlots(9, 2, 8, 18, 'container'),
|
|
@@ -161,7 +162,7 @@ export const inventoryDefinitions = makeInventoryDefinitions({
|
|
|
161
162
|
title: 'Container',
|
|
162
163
|
backgroundTexture: '1.21.11/textures/gui/container/generic_54.png',
|
|
163
164
|
backgroundWidth: 176,
|
|
164
|
-
backgroundHeight:
|
|
165
|
+
backgroundHeight: 168, // 3*18 + 114
|
|
165
166
|
containerRows: 3,
|
|
166
167
|
playerInventoryOffset: { x: 8, y: 84 },
|
|
167
168
|
slots: [
|
|
@@ -175,7 +176,7 @@ export const inventoryDefinitions = makeInventoryDefinitions({
|
|
|
175
176
|
title: 'Container',
|
|
176
177
|
backgroundTexture: '1.21.11/textures/gui/container/generic_54.png',
|
|
177
178
|
backgroundWidth: 176,
|
|
178
|
-
backgroundHeight:
|
|
179
|
+
backgroundHeight: 186, // 4*18 + 114
|
|
179
180
|
containerRows: 4,
|
|
180
181
|
slots: [
|
|
181
182
|
...gridSlots(9, 4, 8, 18, 'container'),
|
|
@@ -188,7 +189,7 @@ export const inventoryDefinitions = makeInventoryDefinitions({
|
|
|
188
189
|
title: 'Container',
|
|
189
190
|
backgroundTexture: '1.21.11/textures/gui/container/generic_54.png',
|
|
190
191
|
backgroundWidth: 176,
|
|
191
|
-
backgroundHeight:
|
|
192
|
+
backgroundHeight: 204, // 5*18 + 114
|
|
192
193
|
containerRows: 5,
|
|
193
194
|
slots: [
|
|
194
195
|
...gridSlots(9, 5, 8, 18, 'container'),
|
|
@@ -201,7 +202,7 @@ export const inventoryDefinitions = makeInventoryDefinitions({
|
|
|
201
202
|
title: 'Container',
|
|
202
203
|
backgroundTexture: '1.21.11/textures/gui/container/generic_54.png',
|
|
203
204
|
backgroundWidth: 176,
|
|
204
|
-
backgroundHeight: 222, // full 6-row texture, no stitching needed
|
|
205
|
+
backgroundHeight: 222, // full 6-row texture, no stitching needed (N*18+114 = 222)
|
|
205
206
|
playerInventoryOffset: { x: 8, y: 140 },
|
|
206
207
|
slots: [
|
|
207
208
|
...gridSlots(9, 6, 8, 18, 'container'),
|
package/src/types.ts
CHANGED
|
@@ -19,6 +19,12 @@ export interface ItemStack {
|
|
|
19
19
|
* Example: `"item/dye_black"` or `"entity/spider/spider"`
|
|
20
20
|
*/
|
|
21
21
|
textureKey?: string
|
|
22
|
+
/**
|
|
23
|
+
* Arbitrary debug identifier exposed as a `data-debug` attribute on the slot element.
|
|
24
|
+
* The mineflayer connector sets this to `"<type>:<metadata>"` by default, making it easy
|
|
25
|
+
* to inspect raw item IDs in browser DevTools without touching React internals.
|
|
26
|
+
*/
|
|
27
|
+
debugKey?: string
|
|
22
28
|
}
|
|
23
29
|
|
|
24
30
|
export interface SlotState {
|