minecraft-inventory 0.1.0 → 0.1.2
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/README.md +80 -10
- package/package.json +20 -8
- package/src/components/CursorItem/CursorItem.tsx +1 -10
- package/src/components/InventoryOverlay/InventoryOverlay.tsx +126 -56
- package/src/components/InventoryWindow/HotbarExtras.tsx +180 -0
- package/src/components/InventoryWindow/InventoryBackground.tsx +3 -3
- package/src/components/InventoryWindow/InventoryWindow.tsx +11 -0
- package/src/components/JEI/JEI.tsx +98 -29
- package/src/components/Notes/Notes.tsx +237 -0
- package/src/components/Notes/index.ts +2 -0
- package/src/components/RecipeGuide/RecipeInventoryView.tsx +5 -15
- package/src/components/Slot/Slot.tsx +49 -17
- package/src/components/Text/MessageFormatted.tsx +1 -1
- package/src/components/Tooltip/Tooltip.module.css +14 -14
- package/src/components/Tooltip/Tooltip.tsx +31 -24
- package/src/connector/demo.ts +4 -0
- package/src/connector/mineflayer.ts +145 -1
- package/src/connector/types.ts +39 -9
- package/src/context/TextureContext.tsx +17 -11
- package/src/generated/localTextures.ts +112 -0
- package/src/globals.d.ts +4 -0
- package/src/index.tsx +3 -2
- package/src/registry/index.ts +1 -0
- package/src/registry/inventories.ts +203 -195
- package/src/types.ts +4 -2
- package/src/utils/globalMouse.ts +14 -0
- package/src/components/Hotbar/Hotbar.tsx +0 -180
- package/src/components/Hotbar/index.ts +0 -1
package/README.md
CHANGED
|
@@ -130,7 +130,6 @@ import { InventoryOverlay, InventoryProvider, ScaleProvider, TextureProvider } f
|
|
|
130
130
|
type="chest"
|
|
131
131
|
showBackdrop // default: true (50% black)
|
|
132
132
|
backdropColor="rgba(0,0,0,0.5)"
|
|
133
|
-
showHotbar // default: true
|
|
134
133
|
showJEI
|
|
135
134
|
jeiItems={items}
|
|
136
135
|
jeiPosition="right"
|
|
@@ -255,7 +254,24 @@ import { TextureProvider } from 'minecraft-inventory'
|
|
|
255
254
|
</TextureProvider>
|
|
256
255
|
```
|
|
257
256
|
|
|
258
|
-
|
|
257
|
+
**Bundled GUI textures (mc-assets)** — Use the optional `mc-assets` package to bundle container backgrounds locally and avoid remote requests. Generate the texture map from your inventory registry, then pass the config to `TextureProvider`:
|
|
258
|
+
|
|
259
|
+
```bash
|
|
260
|
+
pnpm add mc-assets # optional peer dependency
|
|
261
|
+
```
|
|
262
|
+
|
|
263
|
+
```tsx
|
|
264
|
+
import { TextureProvider, InventoryOverlay } from 'minecraft-inventory'
|
|
265
|
+
import { localTexturesConfig } from './generated/localTextures'
|
|
266
|
+
|
|
267
|
+
// GUI container backgrounds (chest, furnace, etc.) load from bundled mc-assets;
|
|
268
|
+
// item/block textures still use the default remote URLs unless you override them.
|
|
269
|
+
<TextureProvider config={localTexturesConfig}>
|
|
270
|
+
<InventoryOverlay type="chest" ... />
|
|
271
|
+
</TextureProvider>
|
|
272
|
+
```
|
|
273
|
+
|
|
274
|
+
The generated file imports every `backgroundTexture` from your registry, resolves each to `node_modules/mc-assets/dist/other-textures/<version>/...` (or `latest/` if that version is missing), and exports `allContainerPaths` (inventory name → short path) plus `localTexturesConfig.getGuiTextureUrl(path)` for use with `<TextureProvider config={...}>`. Re-run `pnpm gen:textures` after changing [inventory types](#adding-new-inventory-types).
|
|
259
275
|
|
|
260
276
|
---
|
|
261
277
|
|
|
@@ -265,20 +281,74 @@ The connector is the bridge between the GUI and a Minecraft server or bot.
|
|
|
265
281
|
|
|
266
282
|
### Mineflayer connector
|
|
267
283
|
|
|
268
|
-
|
|
269
|
-
import { createMineflayerConnector } from 'minecraft-inventory'
|
|
270
|
-
import mineflayer from 'mineflayer'
|
|
284
|
+
Use `createMineflayerConnector(bot)` to plug the GUI into a [mineflayer](https://github.com/PrismarineJS/mineflayer) bot. The connector turns slot clicks, drags, and drops into `bot.clickWindow()` (and plugin APIs for villager trades, enchantment table, anvil, beacon), and subscribes to mineflayer inventory events so the UI stays in sync.
|
|
271
285
|
|
|
272
|
-
|
|
286
|
+
**Example — overlay when a container opens:**
|
|
273
287
|
|
|
274
|
-
|
|
275
|
-
|
|
288
|
+
```tsx
|
|
289
|
+
import React, { useState, useEffect } from 'react'
|
|
290
|
+
import { createRoot } from 'react-dom/client'
|
|
291
|
+
import mineflayer from 'mineflayer'
|
|
292
|
+
import {
|
|
293
|
+
InventoryProvider,
|
|
294
|
+
ScaleProvider,
|
|
295
|
+
TextureProvider,
|
|
296
|
+
InventoryOverlay,
|
|
297
|
+
createMineflayerConnector,
|
|
298
|
+
} from 'minecraft-inventory'
|
|
276
299
|
|
|
277
|
-
|
|
300
|
+
const bot = mineflayer.createBot({
|
|
301
|
+
host: 'localhost',
|
|
302
|
+
port: 25565,
|
|
303
|
+
username: 'InventoryViewer',
|
|
278
304
|
})
|
|
305
|
+
|
|
306
|
+
function App() {
|
|
307
|
+
const [connector, setConnector] = useState(null)
|
|
308
|
+
const [windowType, setWindowType] = useState(null)
|
|
309
|
+
const [open, setOpen] = useState(false)
|
|
310
|
+
|
|
311
|
+
useEffect(() => {
|
|
312
|
+
const onOpen = () => {
|
|
313
|
+
setConnector(createMineflayerConnector(bot))
|
|
314
|
+
setWindowType(bot.currentWindow?.type ?? 'generic_9x1')
|
|
315
|
+
setOpen(true)
|
|
316
|
+
}
|
|
317
|
+
const onClose = () => {
|
|
318
|
+
setOpen(false)
|
|
319
|
+
setConnector(null)
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
bot.on('windowOpen', onOpen)
|
|
323
|
+
bot.on('windowClose', onClose)
|
|
324
|
+
return () => {
|
|
325
|
+
bot.removeListener('windowOpen', onOpen)
|
|
326
|
+
bot.removeListener('windowClose', onClose)
|
|
327
|
+
}
|
|
328
|
+
}, [])
|
|
329
|
+
|
|
330
|
+
if (!open || !connector || !windowType) return null
|
|
331
|
+
|
|
332
|
+
return (
|
|
333
|
+
<TextureProvider>
|
|
334
|
+
<ScaleProvider scale={2}>
|
|
335
|
+
<InventoryProvider connector={connector}>
|
|
336
|
+
<InventoryOverlay
|
|
337
|
+
type={windowType}
|
|
338
|
+
onClose={() => bot.closeWindow(bot.currentWindow)}
|
|
339
|
+
showJEI
|
|
340
|
+
jeiItems={[]}
|
|
341
|
+
/>
|
|
342
|
+
</InventoryProvider>
|
|
343
|
+
</ScaleProvider>
|
|
344
|
+
</TextureProvider>
|
|
345
|
+
)
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
createRoot(document.getElementById('root')).render(<App />)
|
|
279
349
|
```
|
|
280
350
|
|
|
281
|
-
|
|
351
|
+
**Hotbar “open inventory” button:** If you render a hotbar with the `container` option (e.g. mobile open-inventory button), the connector handles the `open-inventory` action: it calls `openPlayerInventory()`, which opens the player inventory GUI, or the ridden entity’s inventory (e.g. llama) when mounted. No extra wiring needed once the connector is passed to `InventoryProvider`.
|
|
282
352
|
|
|
283
353
|
### Demo connector (for local testing)
|
|
284
354
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "minecraft-inventory",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.2",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"release": {
|
|
@@ -17,13 +17,24 @@
|
|
|
17
17
|
"@types/node": "^25.4.0",
|
|
18
18
|
"@types/react": "^19.2.14",
|
|
19
19
|
"@types/react-dom": "^19.2.3",
|
|
20
|
+
"mc-assets": "*",
|
|
21
|
+
"react": "^19.2.4",
|
|
22
|
+
"react-dom": "^19.2.4",
|
|
20
23
|
"minecraft-data": "^3.105.0",
|
|
21
|
-
"typescript": "^5.9.3"
|
|
24
|
+
"typescript": "^5.9.3",
|
|
25
|
+
"@xmcl/text-component": "^2.1.3"
|
|
22
26
|
},
|
|
23
|
-
"dependencies": {
|
|
24
|
-
|
|
25
|
-
"react": "^19.
|
|
26
|
-
"react-dom": "^19.
|
|
27
|
+
"dependencies": {},
|
|
28
|
+
"peerDependencies": {
|
|
29
|
+
"react": "^18.0.0 || ^19.0.0",
|
|
30
|
+
"react-dom": "^18.0.0 || ^19.0.0",
|
|
31
|
+
"@xmcl/text-component": "*",
|
|
32
|
+
"mc-assets": "*"
|
|
33
|
+
},
|
|
34
|
+
"peerDependenciesMeta": {
|
|
35
|
+
"mc-assets": {
|
|
36
|
+
"optional": true
|
|
37
|
+
}
|
|
27
38
|
},
|
|
28
39
|
"repository": "https://github.com/zardoy/minecraft-inventory",
|
|
29
40
|
"engines": {
|
|
@@ -31,7 +42,8 @@
|
|
|
31
42
|
},
|
|
32
43
|
"scripts": {
|
|
33
44
|
"dev": "rsbuild dev",
|
|
34
|
-
"build": "rsbuild build",
|
|
35
|
-
"preview": "rsbuild preview"
|
|
45
|
+
"build": "pnpm gen:textures && rsbuild build",
|
|
46
|
+
"preview": "rsbuild preview",
|
|
47
|
+
"gen:textures": "node scripts/generate-texture-imports.mjs"
|
|
36
48
|
}
|
|
37
49
|
}
|
|
@@ -3,16 +3,7 @@ import { useInventoryContext } from '../../context/InventoryContext'
|
|
|
3
3
|
import { useScale } from '../../context/ScaleContext'
|
|
4
4
|
import { ItemCanvas } from '../ItemCanvas'
|
|
5
5
|
import { useMobile } from '../../hooks/useMobile'
|
|
6
|
-
|
|
7
|
-
// Global mouse tracker — always knows cursor position, even before item is picked up
|
|
8
|
-
const globalMouse = { x: -9999, y: -9999 }
|
|
9
|
-
|
|
10
|
-
if (typeof window !== 'undefined') {
|
|
11
|
-
window.addEventListener('mousemove', (e) => {
|
|
12
|
-
globalMouse.x = e.clientX
|
|
13
|
-
globalMouse.y = e.clientY
|
|
14
|
-
}, { passive: true, capture: true })
|
|
15
|
-
}
|
|
6
|
+
import { globalMouse } from '../../utils/globalMouse'
|
|
16
7
|
|
|
17
8
|
/**
|
|
18
9
|
* CursorItem — optimized to bypass React re-renders on mouse move.
|
|
@@ -1,17 +1,24 @@
|
|
|
1
1
|
import React, { useCallback, useState } from 'react'
|
|
2
2
|
import { useInventoryContext } from '../../context/InventoryContext'
|
|
3
3
|
import { useScale } from '../../context/ScaleContext'
|
|
4
|
+
import { getInventoryType } from '../../registry'
|
|
4
5
|
import { InventoryWindow } from '../InventoryWindow'
|
|
5
6
|
import { CursorItem } from '../CursorItem'
|
|
6
|
-
import { Hotbar } from '../Hotbar'
|
|
7
7
|
import { JEI } from '../JEI'
|
|
8
8
|
import type { JEIItem } from '../JEI'
|
|
9
|
+
import { Notes } from '../Notes'
|
|
10
|
+
import type { Note } from '../Notes'
|
|
9
11
|
import { RecipeInventoryView } from '../RecipeGuide'
|
|
10
12
|
import type { RecipeGuide, RecipeNavFrame } from '../../types'
|
|
11
13
|
|
|
12
14
|
export interface InventoryOverlayProps {
|
|
13
15
|
type: string
|
|
14
16
|
title?: string
|
|
17
|
+
/**
|
|
18
|
+
* Extra properties forwarded to InventoryWindow. For the 'hotbar' type these
|
|
19
|
+
* control special features: `showOffhand` (1/0) and `container` (1/0).
|
|
20
|
+
*/
|
|
21
|
+
properties?: Record<string, number>
|
|
15
22
|
/** Show semi-transparent backdrop behind inventory (default: true) */
|
|
16
23
|
showBackdrop?: boolean
|
|
17
24
|
/** Backdrop color (default: 'rgba(0,0,0,0.5)') */
|
|
@@ -24,18 +31,30 @@ export interface InventoryOverlayProps {
|
|
|
24
31
|
jeiPosition?: 'left' | 'right'
|
|
25
32
|
jeiOnGetRecipes?: (item: JEIItem) => RecipeGuide[]
|
|
26
33
|
jeiOnGetUsages?: (item: JEIItem) => RecipeGuide[]
|
|
27
|
-
/**
|
|
28
|
-
|
|
34
|
+
/** Enable Notes sidebar (uses localStorage by default if callbacks not provided) */
|
|
35
|
+
enableNotes?: boolean
|
|
36
|
+
/** Callback to get notes. If not provided and enableNotes is true, uses localStorage. */
|
|
37
|
+
notesOnGet?: () => Note[] | Promise<Note[]>
|
|
38
|
+
/** Callback to save notes. If not provided and enableNotes is true, uses localStorage. */
|
|
39
|
+
notesOnSave?: (notes: Note[]) => void | Promise<void>
|
|
40
|
+
/** Storage key for localStorage (default: 'mc-inv-notes') */
|
|
41
|
+
notesStorageKey?: string
|
|
42
|
+
/**
|
|
43
|
+
* Content shown in the left side-panel area (e.g. custom panels).
|
|
44
|
+
* Width is auto-computed as (overlayWidth - inventoryWidth) / 2 - padding.
|
|
45
|
+
* Note: If enableNotes is true, Notes will be shown in addition to leftPanel.
|
|
46
|
+
*/
|
|
47
|
+
leftPanel?: React.ReactNode
|
|
29
48
|
className?: string
|
|
30
49
|
style?: React.CSSProperties
|
|
31
|
-
/** Style applied to the inner content container (around inventory + JEI) */
|
|
32
|
-
contentStyle?: React.CSSProperties
|
|
33
50
|
children?: React.ReactNode
|
|
51
|
+
debugBounds?: boolean
|
|
34
52
|
}
|
|
35
53
|
|
|
36
54
|
export function InventoryOverlay({
|
|
37
55
|
type,
|
|
38
56
|
title,
|
|
57
|
+
properties,
|
|
39
58
|
showBackdrop = true,
|
|
40
59
|
backdropColor = 'rgba(0,0,0,0.5)',
|
|
41
60
|
onClose,
|
|
@@ -44,15 +63,26 @@ export function InventoryOverlay({
|
|
|
44
63
|
jeiPosition = 'right',
|
|
45
64
|
jeiOnGetRecipes,
|
|
46
65
|
jeiOnGetUsages,
|
|
47
|
-
|
|
66
|
+
enableNotes = false,
|
|
67
|
+
notesOnGet,
|
|
68
|
+
notesOnSave,
|
|
69
|
+
notesStorageKey,
|
|
70
|
+
leftPanel,
|
|
48
71
|
className,
|
|
49
72
|
style,
|
|
50
|
-
contentStyle,
|
|
51
73
|
children,
|
|
74
|
+
debugBounds = false,
|
|
52
75
|
}: InventoryOverlayProps) {
|
|
53
76
|
const { heldItem, sendAction, setHeldItem } = useInventoryContext()
|
|
54
77
|
const { scale } = useScale()
|
|
55
78
|
|
|
79
|
+
const def = getInventoryType(type)
|
|
80
|
+
const invUnscaledW = def?.backgroundWidth ?? 176
|
|
81
|
+
const sideGapPx = 5 * scale // gap from overlay edge and from inventory edge
|
|
82
|
+
|
|
83
|
+
// Full height for side panels = overlay height minus edge gaps
|
|
84
|
+
const sidePanelHeight = `calc(100% - ${sideGapPx * 2}px)`
|
|
85
|
+
|
|
56
86
|
// Recipe navigation stack — when non-empty, RecipeInventoryView replaces InventoryWindow
|
|
57
87
|
const [recipeNavStack, setRecipeNavStack] = useState<RecipeNavFrame[]>([])
|
|
58
88
|
|
|
@@ -102,6 +132,26 @@ export function InventoryOverlay({
|
|
|
102
132
|
/>
|
|
103
133
|
) : null
|
|
104
134
|
|
|
135
|
+
const notesPanel = enableNotes ? (
|
|
136
|
+
<Notes
|
|
137
|
+
onGetNotes={notesOnGet}
|
|
138
|
+
onSaveNotes={notesOnSave}
|
|
139
|
+
storageKey={notesStorageKey}
|
|
140
|
+
/>
|
|
141
|
+
) : null
|
|
142
|
+
|
|
143
|
+
// Full height side panels; width fills the space between overlay edge and inventory center
|
|
144
|
+
const sidePanelBase: React.CSSProperties = {
|
|
145
|
+
position: 'absolute',
|
|
146
|
+
top: sideGapPx,
|
|
147
|
+
bottom: sideGapPx,
|
|
148
|
+
width: `calc(100% / 2 - ${invUnscaledW * scale}px / 2 - ${sideGapPx}px * 2)`,
|
|
149
|
+
height: sidePanelHeight,
|
|
150
|
+
zIndex: 5,
|
|
151
|
+
display: 'flex',
|
|
152
|
+
flexDirection: 'column',
|
|
153
|
+
}
|
|
154
|
+
|
|
105
155
|
return (
|
|
106
156
|
<>
|
|
107
157
|
<div
|
|
@@ -110,39 +160,56 @@ export function InventoryOverlay({
|
|
|
110
160
|
style={{
|
|
111
161
|
position: 'absolute',
|
|
112
162
|
inset: 0,
|
|
163
|
+
width: '100%',
|
|
164
|
+
height: '100%',
|
|
113
165
|
background: showBackdrop ? backdropColor : 'transparent',
|
|
114
166
|
cursor: 'default',
|
|
115
167
|
zIndex: 1,
|
|
116
168
|
...style,
|
|
117
169
|
}}
|
|
118
170
|
>
|
|
119
|
-
{/*
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
left: '
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
171
|
+
{/* ── Left side panel (leftPanel prop, Notes, and/or JEI-left) ── */}
|
|
172
|
+
{(leftPanel || enableNotes || (showJEI && jeiPosition === 'left')) && (
|
|
173
|
+
<div
|
|
174
|
+
className="mc-inv-overlay-side mc-inv-overlay-side-left"
|
|
175
|
+
onClick={(e) => e.stopPropagation()}
|
|
176
|
+
style={{ ...sidePanelBase, left: sideGapPx, pointerEvents: 'auto' }}
|
|
177
|
+
>
|
|
178
|
+
{enableNotes && (
|
|
179
|
+
<div className="mc-inv-overlay-notes" style={{ flex: 1, minHeight: 0, display: 'flex', flexDirection: 'column' }}>
|
|
180
|
+
{notesPanel}
|
|
181
|
+
</div>
|
|
182
|
+
)}
|
|
183
|
+
{leftPanel && (
|
|
184
|
+
<div style={{ flex: 1, minHeight: 0, display: 'flex', flexDirection: 'column' }}>
|
|
185
|
+
{leftPanel}
|
|
186
|
+
</div>
|
|
187
|
+
)}
|
|
131
188
|
{showJEI && jeiPosition === 'left' && (
|
|
132
189
|
<div
|
|
133
190
|
className="mc-inv-overlay-jei mc-inv-overlay-jei-left"
|
|
134
|
-
|
|
135
|
-
style={{
|
|
136
|
-
position: 'absolute',
|
|
137
|
-
right: `calc(100% + ${4 * scale}px)`,
|
|
138
|
-
top: 0,
|
|
139
|
-
zIndex: 5,
|
|
140
|
-
}}
|
|
191
|
+
style={{ flex: 1, minHeight: 0, display: 'flex', flexDirection: 'column', width: '100%' }}
|
|
141
192
|
>
|
|
142
193
|
{jeiPanel}
|
|
143
194
|
</div>
|
|
144
195
|
)}
|
|
196
|
+
</div>
|
|
197
|
+
)}
|
|
145
198
|
|
|
199
|
+
{/* ── Inventory centered anchor — flex centering, no transform ── */}
|
|
200
|
+
<div
|
|
201
|
+
className="mc-inv-overlay-center"
|
|
202
|
+
style={{
|
|
203
|
+
position: 'absolute',
|
|
204
|
+
inset: 0,
|
|
205
|
+
width: '100%',
|
|
206
|
+
height: '100%',
|
|
207
|
+
display: 'flex',
|
|
208
|
+
justifyContent: 'center',
|
|
209
|
+
alignItems: 'center',
|
|
210
|
+
}}
|
|
211
|
+
>
|
|
212
|
+
<div className="mc-inv-overlay-content" style={{ position: 'relative' }}>
|
|
146
213
|
{/* Inventory / Recipe view — stopPropagation so clicks inside don't close the overlay */}
|
|
147
214
|
<div className="mc-inv-overlay-window" onClick={(e) => e.stopPropagation()}>
|
|
148
215
|
{recipeNavStack.length > 0 ? (
|
|
@@ -153,41 +220,44 @@ export function InventoryOverlay({
|
|
|
153
220
|
onPushFrame={handleRecipePushFrame}
|
|
154
221
|
/>
|
|
155
222
|
) : (
|
|
156
|
-
<InventoryWindow type={type} title={title} />
|
|
223
|
+
<InventoryWindow type={type} title={title} properties={properties} />
|
|
157
224
|
)}
|
|
158
225
|
</div>
|
|
226
|
+
</div>
|
|
227
|
+
</div>
|
|
159
228
|
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
229
|
+
{/* ── Right side panel (JEI-right) ── */}
|
|
230
|
+
{showJEI && jeiPosition === 'right' && (
|
|
231
|
+
<div
|
|
232
|
+
className="mc-inv-overlay-side mc-inv-overlay-side-right"
|
|
233
|
+
onClick={(e) => e.stopPropagation()}
|
|
234
|
+
style={{ ...sidePanelBase, right: sideGapPx, pointerEvents: 'auto' }}
|
|
235
|
+
>
|
|
236
|
+
<div
|
|
237
|
+
className="mc-inv-overlay-jei mc-inv-overlay-jei-right"
|
|
238
|
+
style={{ flex: 1, minHeight: 0, display: 'flex', flexDirection: 'column', width: '100%' }}
|
|
239
|
+
>
|
|
240
|
+
{jeiPanel}
|
|
241
|
+
</div>
|
|
242
|
+
</div>
|
|
243
|
+
)}
|
|
170
244
|
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
<div
|
|
174
|
-
className="mc-inv-overlay-jei mc-inv-overlay-jei-right"
|
|
175
|
-
onClick={(e) => e.stopPropagation()}
|
|
176
|
-
style={{
|
|
177
|
-
position: 'absolute',
|
|
178
|
-
left: `calc(100% + ${4 * scale}px)`,
|
|
179
|
-
top: 0,
|
|
180
|
-
zIndex: 5,
|
|
181
|
-
}}
|
|
182
|
-
>
|
|
183
|
-
{jeiPanel}
|
|
184
|
-
</div>
|
|
185
|
-
)}
|
|
245
|
+
{/* Extra children (overlay-level) */}
|
|
246
|
+
{children}
|
|
186
247
|
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
248
|
+
{/* Debug bounds */}
|
|
249
|
+
{debugBounds && (
|
|
250
|
+
<div
|
|
251
|
+
className="mc-inv-overlay-debug-marker"
|
|
252
|
+
style={{
|
|
253
|
+
position: 'absolute',
|
|
254
|
+
inset: 0,
|
|
255
|
+
border: '2px solid yellow',
|
|
256
|
+
pointerEvents: 'none',
|
|
257
|
+
boxSizing: 'border-box',
|
|
258
|
+
}}
|
|
259
|
+
/>
|
|
260
|
+
)}
|
|
191
261
|
</div>
|
|
192
262
|
|
|
193
263
|
<CursorItem />
|
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
import React from 'react'
|
|
2
|
+
import { useInventoryContext } from '../../context/InventoryContext'
|
|
3
|
+
import { useScale } from '../../context/ScaleContext'
|
|
4
|
+
import { useTextures } from '../../context/TextureContext'
|
|
5
|
+
import { Slot } from '../Slot'
|
|
6
|
+
import type { ItemStack } from '../../types'
|
|
7
|
+
|
|
8
|
+
interface HotbarExtrasProps {
|
|
9
|
+
showOffhand: boolean
|
|
10
|
+
container: boolean
|
|
11
|
+
offhandItem: ItemStack | null
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/** Renders a sprite from the widgets.png using the same inner-div+transform approach as InventoryBackground */
|
|
15
|
+
function WidgetSprite({
|
|
16
|
+
url,
|
|
17
|
+
srcX,
|
|
18
|
+
srcY,
|
|
19
|
+
srcW,
|
|
20
|
+
srcH,
|
|
21
|
+
scale,
|
|
22
|
+
style,
|
|
23
|
+
}: {
|
|
24
|
+
url: string
|
|
25
|
+
srcX: number
|
|
26
|
+
srcY: number
|
|
27
|
+
srcW: number
|
|
28
|
+
srcH: number
|
|
29
|
+
scale: number
|
|
30
|
+
style?: React.CSSProperties
|
|
31
|
+
}) {
|
|
32
|
+
return (
|
|
33
|
+
<div
|
|
34
|
+
style={{
|
|
35
|
+
position: 'absolute',
|
|
36
|
+
width: srcW * scale,
|
|
37
|
+
height: srcH * scale,
|
|
38
|
+
overflow: 'hidden',
|
|
39
|
+
imageRendering: 'pixelated',
|
|
40
|
+
pointerEvents: 'none',
|
|
41
|
+
...style,
|
|
42
|
+
}}
|
|
43
|
+
>
|
|
44
|
+
<div
|
|
45
|
+
style={{
|
|
46
|
+
width: srcW,
|
|
47
|
+
height: srcH,
|
|
48
|
+
overflow: 'hidden',
|
|
49
|
+
transform: `scale(${scale})`,
|
|
50
|
+
transformOrigin: 'top left',
|
|
51
|
+
}}
|
|
52
|
+
>
|
|
53
|
+
<img
|
|
54
|
+
src={url}
|
|
55
|
+
alt=""
|
|
56
|
+
aria-hidden
|
|
57
|
+
draggable={false}
|
|
58
|
+
style={{
|
|
59
|
+
position: 'absolute',
|
|
60
|
+
top: -srcY,
|
|
61
|
+
left: -srcX,
|
|
62
|
+
imageRendering: 'pixelated',
|
|
63
|
+
userSelect: 'none',
|
|
64
|
+
}}
|
|
65
|
+
/>
|
|
66
|
+
</div>
|
|
67
|
+
</div>
|
|
68
|
+
)
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Hotbar-specific extras rendered inside InventoryBackground alongside the regular slots.
|
|
73
|
+
* Mirrors layouts.mjs Hotbar children:
|
|
74
|
+
* - Active slot indicator (always)
|
|
75
|
+
* - Offhand slot left of the strip (when showOffhand)
|
|
76
|
+
* - Open-inventory button right of the strip (when container)
|
|
77
|
+
*/
|
|
78
|
+
export function HotbarExtras({ showOffhand, container, offhandItem }: HotbarExtrasProps) {
|
|
79
|
+
const { playerState, sendAction } = useInventoryContext()
|
|
80
|
+
const { scale } = useScale()
|
|
81
|
+
const textures = useTextures()
|
|
82
|
+
|
|
83
|
+
const activeSlot = playerState?.activeHotbarSlot ?? 0
|
|
84
|
+
const hotbarUrl = textures.getGuiTextureUrl('1.15/textures/gui/widgets.png')
|
|
85
|
+
|
|
86
|
+
// Sprite regions within widgets.png (native pixels):
|
|
87
|
+
// Hotbar strip : [0, 0, 182, 22]
|
|
88
|
+
// Active box : [0, 22, 24, 24]
|
|
89
|
+
// Offhand box : [24, 22, 24, 24]
|
|
90
|
+
const GAP = 2
|
|
91
|
+
|
|
92
|
+
return (
|
|
93
|
+
<>
|
|
94
|
+
{/* ── Active slot indicator ── */}
|
|
95
|
+
<WidgetSprite
|
|
96
|
+
url={hotbarUrl}
|
|
97
|
+
srcX={0}
|
|
98
|
+
srcY={22}
|
|
99
|
+
srcW={24}
|
|
100
|
+
srcH={24}
|
|
101
|
+
scale={scale}
|
|
102
|
+
style={{
|
|
103
|
+
left: (activeSlot * 20 - 1) * scale,
|
|
104
|
+
top: -1 * scale,
|
|
105
|
+
zIndex: 2,
|
|
106
|
+
}}
|
|
107
|
+
/>
|
|
108
|
+
|
|
109
|
+
{/* ── Offhand slot (left of hotbar) ── */}
|
|
110
|
+
{showOffhand && (
|
|
111
|
+
<div
|
|
112
|
+
className="mc-inv-hotbar-offhand"
|
|
113
|
+
style={{
|
|
114
|
+
position: 'absolute',
|
|
115
|
+
left: -(24 + GAP) * scale,
|
|
116
|
+
top: 0,
|
|
117
|
+
width: 24 * scale,
|
|
118
|
+
height: 22 * scale,
|
|
119
|
+
}}
|
|
120
|
+
>
|
|
121
|
+
<WidgetSprite
|
|
122
|
+
url={hotbarUrl}
|
|
123
|
+
srcX={24}
|
|
124
|
+
srcY={22}
|
|
125
|
+
srcW={24}
|
|
126
|
+
srcH={22}
|
|
127
|
+
scale={scale}
|
|
128
|
+
style={{ left: 0, top: 0 }}
|
|
129
|
+
/>
|
|
130
|
+
{/* Item at slot 45 (offhand) */}
|
|
131
|
+
<div style={{ position: 'absolute', top: 3 * scale, left: 3 * scale }}>
|
|
132
|
+
<Slot index={45} item={offhandItem} size={16 * scale} noBackground />
|
|
133
|
+
</div>
|
|
134
|
+
</div>
|
|
135
|
+
)}
|
|
136
|
+
|
|
137
|
+
{/* ── Open-inventory button (right of hotbar) ── */}
|
|
138
|
+
{container && (
|
|
139
|
+
<div
|
|
140
|
+
className="mc-inv-hotbar-open-inv"
|
|
141
|
+
title="Open inventory"
|
|
142
|
+
style={{
|
|
143
|
+
position: 'absolute',
|
|
144
|
+
left: (182 + GAP) * scale,
|
|
145
|
+
top: 0,
|
|
146
|
+
width: 24 * scale,
|
|
147
|
+
height: 22 * scale,
|
|
148
|
+
cursor: 'pointer',
|
|
149
|
+
}}
|
|
150
|
+
onClick={() => sendAction({ type: 'open-inventory' })}
|
|
151
|
+
>
|
|
152
|
+
<WidgetSprite
|
|
153
|
+
url={hotbarUrl}
|
|
154
|
+
srcX={24}
|
|
155
|
+
srcY={22}
|
|
156
|
+
srcW={24}
|
|
157
|
+
srcH={22}
|
|
158
|
+
scale={scale}
|
|
159
|
+
style={{ left: 0, top: 0 }}
|
|
160
|
+
/>
|
|
161
|
+
{/* Three white dots centered */}
|
|
162
|
+
{[0, 1, 2].map((i) => (
|
|
163
|
+
<div
|
|
164
|
+
key={i}
|
|
165
|
+
style={{
|
|
166
|
+
position: 'absolute',
|
|
167
|
+
left: (6 + i * 4) * scale,
|
|
168
|
+
top: 10 * scale,
|
|
169
|
+
width: 2 * scale,
|
|
170
|
+
height: 2 * scale,
|
|
171
|
+
background: 'white',
|
|
172
|
+
pointerEvents: 'none',
|
|
173
|
+
}}
|
|
174
|
+
/>
|
|
175
|
+
))}
|
|
176
|
+
</div>
|
|
177
|
+
)}
|
|
178
|
+
</>
|
|
179
|
+
)
|
|
180
|
+
}
|
|
@@ -18,7 +18,7 @@ export function InventoryBackground({
|
|
|
18
18
|
const textures = useTextures()
|
|
19
19
|
const { scale } = useScale()
|
|
20
20
|
|
|
21
|
-
const bgUrl = textures.getGuiTextureUrl(definition.backgroundTexture
|
|
21
|
+
const bgUrl = textures.getGuiTextureUrl(definition.backgroundTexture)
|
|
22
22
|
const w = definition.backgroundWidth * scale
|
|
23
23
|
const h = definition.backgroundHeight * scale
|
|
24
24
|
|
|
@@ -80,8 +80,8 @@ export function InventoryBackground({
|
|
|
80
80
|
className="mc-inv-background-title"
|
|
81
81
|
style={{
|
|
82
82
|
position: 'absolute',
|
|
83
|
-
top: 6 * scale,
|
|
84
|
-
left: 8 * scale,
|
|
83
|
+
top: (6 + (definition.titleOffset?.y ?? 0)) * scale,
|
|
84
|
+
left: (8 + (definition.titleOffset?.x ?? 0)) * scale,
|
|
85
85
|
fontSize: 7 * scale,
|
|
86
86
|
fontFamily: "'Minecraftia', 'Minecraft', monospace",
|
|
87
87
|
pointerEvents: 'none',
|
|
@@ -9,6 +9,7 @@ import { InventoryBackground } from './InventoryBackground'
|
|
|
9
9
|
import { ProgressBar } from './ProgressBar'
|
|
10
10
|
import { VillagerTradeList } from './VillagerTradeList'
|
|
11
11
|
import { EnchantmentOptions } from './EnchantmentOptions'
|
|
12
|
+
import { HotbarExtras } from './HotbarExtras'
|
|
12
13
|
|
|
13
14
|
interface InventoryWindowProps {
|
|
14
15
|
type: string
|
|
@@ -58,6 +59,7 @@ export function InventoryWindow({
|
|
|
58
59
|
const effectiveTitle = type === 'player' ? undefined : (title ?? windowState?.title ?? def.title)
|
|
59
60
|
const isVillager = type === 'villager'
|
|
60
61
|
const isEnchanting = type === 'enchanting_table'
|
|
62
|
+
const isHotbar = type === 'hotbar'
|
|
61
63
|
|
|
62
64
|
const resolveItem = (slotIndex: number) => {
|
|
63
65
|
const fromProp = effectiveSlots.find((s) => s.index === slotIndex)
|
|
@@ -114,6 +116,15 @@ export function InventoryWindow({
|
|
|
114
116
|
y={14}
|
|
115
117
|
/>
|
|
116
118
|
)}
|
|
119
|
+
|
|
120
|
+
{/* Hotbar extras: active slot indicator, offhand slot, open-inventory button */}
|
|
121
|
+
{isHotbar && (
|
|
122
|
+
<HotbarExtras
|
|
123
|
+
showOffhand={Boolean(effectiveProperties.showOffhand)}
|
|
124
|
+
container={Boolean(effectiveProperties.container)}
|
|
125
|
+
offhandItem={resolveItem(45)}
|
|
126
|
+
/>
|
|
127
|
+
)}
|
|
117
128
|
</InventoryBackground>
|
|
118
129
|
</div>
|
|
119
130
|
)
|