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
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "minecraft-inventory",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.5",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"release": {
|
|
@@ -42,7 +42,7 @@
|
|
|
42
42
|
},
|
|
43
43
|
"scripts": {
|
|
44
44
|
"dev": "rsbuild dev",
|
|
45
|
-
"build": "pnpm gen:textures && rsbuild build",
|
|
45
|
+
"build": "pnpm gen:textures && node scripts/stamp-version.mjs && rsbuild build",
|
|
46
46
|
"preview": "rsbuild preview",
|
|
47
47
|
"gen:textures": "node scripts/generate-texture-imports.mjs"
|
|
48
48
|
}
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
@@ -19,8 +19,8 @@ export interface InventoryOverlayProps {
|
|
|
19
19
|
* control special features: `showOffhand` (1/0) and `container` (1/0).
|
|
20
20
|
*/
|
|
21
21
|
properties?: Record<string, number>
|
|
22
|
-
/**
|
|
23
|
-
|
|
22
|
+
/** Hide semi-transparent backdrop behind inventory */
|
|
23
|
+
noBackdrop?: boolean
|
|
24
24
|
/** Backdrop color (default: 'rgba(0,0,0,0.5)') */
|
|
25
25
|
backdropColor?: string
|
|
26
26
|
/** Called when clicking outside the inventory (or pressing Esc) */
|
|
@@ -48,14 +48,21 @@ export interface InventoryOverlayProps {
|
|
|
48
48
|
className?: string
|
|
49
49
|
style?: React.CSSProperties
|
|
50
50
|
children?: React.ReactNode
|
|
51
|
+
/** Show yellow debug border around the overlay area */
|
|
51
52
|
debugBounds?: boolean
|
|
53
|
+
/** Show red debug outline around the inventory background */
|
|
54
|
+
showDebug?: boolean
|
|
55
|
+
/** Override entity display rendering. Pass a function returning JSX, or null to hide. */
|
|
56
|
+
renderEntity?: ((width: number, height: number) => React.ReactNode) | null
|
|
57
|
+
/** Hide the "INV" version watermark (opt-out) */
|
|
58
|
+
noWatermark?: boolean
|
|
52
59
|
}
|
|
53
60
|
|
|
54
61
|
export function InventoryOverlay({
|
|
55
62
|
type,
|
|
56
63
|
title,
|
|
57
64
|
properties,
|
|
58
|
-
|
|
65
|
+
noBackdrop = false,
|
|
59
66
|
backdropColor = 'rgba(0,0,0,0.5)',
|
|
60
67
|
onClose,
|
|
61
68
|
showJEI = false,
|
|
@@ -72,8 +79,11 @@ export function InventoryOverlay({
|
|
|
72
79
|
style,
|
|
73
80
|
children,
|
|
74
81
|
debugBounds = false,
|
|
82
|
+
showDebug = false,
|
|
83
|
+
renderEntity,
|
|
84
|
+
noWatermark = false,
|
|
75
85
|
}: InventoryOverlayProps) {
|
|
76
|
-
const { heldItem, sendAction, setHeldItem } = useInventoryContext()
|
|
86
|
+
const { heldItem, sendAction, setHeldItem, focusedSlot, setFocusedSlot } = useInventoryContext()
|
|
77
87
|
const { scale } = useScale()
|
|
78
88
|
|
|
79
89
|
const def = getInventoryType(type)
|
|
@@ -114,13 +124,18 @@ export function InventoryOverlay({
|
|
|
114
124
|
|
|
115
125
|
// Fires for any click that isn't stopped by an interactive panel (inventory, hotbar, JEI, etc.)
|
|
116
126
|
const handleBackdropClick = useCallback(() => {
|
|
127
|
+
// Clicking the backdrop clears focused slot (regardless of heldItem)
|
|
128
|
+
if (focusedSlot !== null) {
|
|
129
|
+
setFocusedSlot(null)
|
|
130
|
+
return
|
|
131
|
+
}
|
|
117
132
|
if (heldItem) {
|
|
118
133
|
sendAction({ type: 'drop', slotIndex: -1, all: true })
|
|
119
134
|
setHeldItem(null)
|
|
120
135
|
} else {
|
|
121
136
|
onClose?.()
|
|
122
137
|
}
|
|
123
|
-
}, [heldItem, sendAction, setHeldItem, onClose])
|
|
138
|
+
}, [heldItem, sendAction, setHeldItem, onClose, focusedSlot, setFocusedSlot])
|
|
124
139
|
|
|
125
140
|
const jeiPanel = showJEI ? (
|
|
126
141
|
<JEI
|
|
@@ -162,7 +177,7 @@ export function InventoryOverlay({
|
|
|
162
177
|
inset: 0,
|
|
163
178
|
width: '100%',
|
|
164
179
|
height: '100%',
|
|
165
|
-
background:
|
|
180
|
+
background: noBackdrop ? 'transparent' : backdropColor,
|
|
166
181
|
cursor: 'default',
|
|
167
182
|
zIndex: 1,
|
|
168
183
|
...style,
|
|
@@ -210,8 +225,15 @@ export function InventoryOverlay({
|
|
|
210
225
|
}}
|
|
211
226
|
>
|
|
212
227
|
<div className="mc-inv-overlay-content" style={{ position: 'relative' }}>
|
|
213
|
-
{/* Inventory / Recipe view —
|
|
214
|
-
<div
|
|
228
|
+
{/* Inventory / Recipe view — clicks clear focused slot; slots stop propagation themselves */}
|
|
229
|
+
<div
|
|
230
|
+
className="mc-inv-overlay-window"
|
|
231
|
+
onClick={(e) => {
|
|
232
|
+
e.stopPropagation()
|
|
233
|
+
// Clicking the inventory background (not a slot) clears focused slot
|
|
234
|
+
if (focusedSlot !== null) setFocusedSlot(null)
|
|
235
|
+
}}
|
|
236
|
+
>
|
|
215
237
|
{recipeNavStack.length > 0 ? (
|
|
216
238
|
<RecipeInventoryView
|
|
217
239
|
navStack={recipeNavStack}
|
|
@@ -220,7 +242,7 @@ export function InventoryOverlay({
|
|
|
220
242
|
onPushFrame={handleRecipePushFrame}
|
|
221
243
|
/>
|
|
222
244
|
) : (
|
|
223
|
-
<InventoryWindow type={type} title={title} properties={properties} showDebug={
|
|
245
|
+
<InventoryWindow type={type} title={title} properties={properties} showDebug={showDebug} renderEntity={renderEntity} />
|
|
224
246
|
)}
|
|
225
247
|
</div>
|
|
226
248
|
</div>
|
|
@@ -245,6 +267,31 @@ export function InventoryOverlay({
|
|
|
245
267
|
{/* Extra children (overlay-level) */}
|
|
246
268
|
{children}
|
|
247
269
|
|
|
270
|
+
{/* Watermark */}
|
|
271
|
+
{!noWatermark && (
|
|
272
|
+
<a
|
|
273
|
+
className="mc-inv-watermark"
|
|
274
|
+
href="https://www.npmjs.com/package/minecraft-inventory"
|
|
275
|
+
target="_blank"
|
|
276
|
+
rel="noopener noreferrer"
|
|
277
|
+
onClick={(e) => e.stopPropagation()}
|
|
278
|
+
style={{
|
|
279
|
+
position: 'absolute',
|
|
280
|
+
top: 4,
|
|
281
|
+
left: 4,
|
|
282
|
+
fontSize: 9,
|
|
283
|
+
fontFamily: "'Minecraftia', 'Minecraft', monospace",
|
|
284
|
+
color: 'rgba(255,255,255,0.3)',
|
|
285
|
+
textDecoration: 'none',
|
|
286
|
+
pointerEvents: 'auto',
|
|
287
|
+
zIndex: 1,
|
|
288
|
+
lineHeight: 1,
|
|
289
|
+
}}
|
|
290
|
+
>
|
|
291
|
+
INV 0.0.0
|
|
292
|
+
</a>
|
|
293
|
+
)}
|
|
294
|
+
|
|
248
295
|
{/* Debug bounds */}
|
|
249
296
|
{debugBounds && (
|
|
250
297
|
<div
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
import React, { useState, useCallback, useRef, useEffect } from 'react'
|
|
2
|
+
import { useScale } from '../../context/ScaleContext'
|
|
3
|
+
import { useTextures } from '../../context/TextureContext'
|
|
4
|
+
import { useInventoryContext } from '../../context/InventoryContext'
|
|
5
|
+
|
|
6
|
+
interface AnvilInputProps {
|
|
7
|
+
x: number
|
|
8
|
+
y: number
|
|
9
|
+
width: number
|
|
10
|
+
height: number
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export function AnvilInput({ x, y, width, height }: AnvilInputProps) {
|
|
14
|
+
const { scale } = useScale()
|
|
15
|
+
const textures = useTextures()
|
|
16
|
+
const { sendAction, windowState } = useInventoryContext()
|
|
17
|
+
|
|
18
|
+
const [text, setText] = useState('')
|
|
19
|
+
const [focused, setFocused] = useState(false)
|
|
20
|
+
const inputRef = useRef<HTMLInputElement>(null)
|
|
21
|
+
|
|
22
|
+
const hasInputItem = windowState?.slots.some((s) => s.index === 0 && s.item !== null) ?? false
|
|
23
|
+
|
|
24
|
+
const textFieldUrl = textures.getGuiTextureUrl(
|
|
25
|
+
hasInputItem
|
|
26
|
+
? '1.21.11/textures/gui/sprites/container/anvil/text_field.png'
|
|
27
|
+
: '1.21.11/textures/gui/sprites/container/anvil/text_field_disabled.png',
|
|
28
|
+
)
|
|
29
|
+
|
|
30
|
+
useEffect(() => {
|
|
31
|
+
if (!hasInputItem) {
|
|
32
|
+
setText('')
|
|
33
|
+
setFocused(false)
|
|
34
|
+
}
|
|
35
|
+
}, [hasInputItem])
|
|
36
|
+
|
|
37
|
+
const handleChange = useCallback(
|
|
38
|
+
(e: React.ChangeEvent<HTMLInputElement>) => {
|
|
39
|
+
const val = e.target.value
|
|
40
|
+
setText(val)
|
|
41
|
+
sendAction({ type: 'rename', text: val })
|
|
42
|
+
},
|
|
43
|
+
[sendAction],
|
|
44
|
+
)
|
|
45
|
+
|
|
46
|
+
return (
|
|
47
|
+
<div
|
|
48
|
+
className="mc-inv-anvil-input"
|
|
49
|
+
style={{
|
|
50
|
+
position: 'absolute',
|
|
51
|
+
left: x * scale,
|
|
52
|
+
top: y * scale,
|
|
53
|
+
width: width * scale,
|
|
54
|
+
height: height * scale,
|
|
55
|
+
imageRendering: 'pixelated',
|
|
56
|
+
}}
|
|
57
|
+
>
|
|
58
|
+
<img
|
|
59
|
+
src={textFieldUrl}
|
|
60
|
+
alt=""
|
|
61
|
+
aria-hidden
|
|
62
|
+
draggable={false}
|
|
63
|
+
style={{
|
|
64
|
+
position: 'absolute',
|
|
65
|
+
inset: 0,
|
|
66
|
+
width: '100%',
|
|
67
|
+
height: '100%',
|
|
68
|
+
imageRendering: 'pixelated',
|
|
69
|
+
pointerEvents: 'none',
|
|
70
|
+
}}
|
|
71
|
+
/>
|
|
72
|
+
<input
|
|
73
|
+
ref={inputRef}
|
|
74
|
+
type="text"
|
|
75
|
+
value={text}
|
|
76
|
+
onChange={handleChange}
|
|
77
|
+
onFocus={() => setFocused(true)}
|
|
78
|
+
onBlur={() => setFocused(false)}
|
|
79
|
+
disabled={!hasInputItem}
|
|
80
|
+
maxLength={35}
|
|
81
|
+
placeholder=""
|
|
82
|
+
className="mc-inv-anvil-rename-input"
|
|
83
|
+
style={{
|
|
84
|
+
position: 'absolute',
|
|
85
|
+
left: 2 * scale,
|
|
86
|
+
top: 1 * scale,
|
|
87
|
+
width: (width - 4) * scale,
|
|
88
|
+
height: (height - 2) * scale,
|
|
89
|
+
border: 'none',
|
|
90
|
+
outline: 'none',
|
|
91
|
+
background: 'transparent',
|
|
92
|
+
color: '#ffffff',
|
|
93
|
+
fontFamily: "'Minecraftia', 'Minecraft', monospace",
|
|
94
|
+
fontSize: 7 * scale,
|
|
95
|
+
padding: 0,
|
|
96
|
+
lineHeight: 1,
|
|
97
|
+
caretColor: focused ? '#ffffff' : 'transparent',
|
|
98
|
+
}}
|
|
99
|
+
/>
|
|
100
|
+
</div>
|
|
101
|
+
)
|
|
102
|
+
}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import React from 'react'
|
|
2
|
+
import { useScale } from '../../context/ScaleContext'
|
|
3
|
+
import type { EntityDisplayArea } from '../../types'
|
|
4
|
+
|
|
5
|
+
interface EntityDisplayProps {
|
|
6
|
+
area: EntityDisplayArea
|
|
7
|
+
renderEntity?: ((width: number, height: number) => React.ReactNode) | null
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export function EntityDisplay({ area, renderEntity }: EntityDisplayProps) {
|
|
11
|
+
const { scale } = useScale()
|
|
12
|
+
|
|
13
|
+
if (renderEntity === null) return null
|
|
14
|
+
|
|
15
|
+
const w = area.width * scale
|
|
16
|
+
const h = area.height * scale
|
|
17
|
+
|
|
18
|
+
return (
|
|
19
|
+
<div
|
|
20
|
+
className="mc-inv-entity-display"
|
|
21
|
+
style={{
|
|
22
|
+
position: 'absolute',
|
|
23
|
+
left: area.x * scale,
|
|
24
|
+
top: area.y * scale,
|
|
25
|
+
width: w,
|
|
26
|
+
height: h,
|
|
27
|
+
overflow: 'hidden',
|
|
28
|
+
}}
|
|
29
|
+
>
|
|
30
|
+
{renderEntity ? (
|
|
31
|
+
renderEntity(w, h)
|
|
32
|
+
) : (
|
|
33
|
+
<div
|
|
34
|
+
className="mc-inv-entity-display-placeholder"
|
|
35
|
+
style={{
|
|
36
|
+
width: '100%',
|
|
37
|
+
height: '100%',
|
|
38
|
+
background: 'rgba(255, 0, 0, 0.15)',
|
|
39
|
+
border: '1px solid rgba(255, 0, 0, 0.4)',
|
|
40
|
+
boxSizing: 'border-box',
|
|
41
|
+
}}
|
|
42
|
+
/>
|
|
43
|
+
)}
|
|
44
|
+
</div>
|
|
45
|
+
)
|
|
46
|
+
}
|
|
@@ -4,6 +4,22 @@ import { useScale } from '../../context/ScaleContext'
|
|
|
4
4
|
import { MessageFormattedString } from '../Text/MessageFormattedString'
|
|
5
5
|
import type { InventoryTypeDefinition } from '../../registry'
|
|
6
6
|
|
|
7
|
+
/**
|
|
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
|
+
*
|
|
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.
|
|
19
|
+
*/
|
|
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
|
|
22
|
+
|
|
7
23
|
interface InventoryBackgroundProps {
|
|
8
24
|
definition: InventoryTypeDefinition
|
|
9
25
|
children: React.ReactNode
|
|
@@ -21,13 +37,23 @@ export function InventoryBackground({
|
|
|
21
37
|
const { scale } = useScale()
|
|
22
38
|
|
|
23
39
|
const bgUrl = textures.getGuiTextureUrl(definition.backgroundTexture)
|
|
40
|
+
const isStitched = definition.containerRows != null && definition.containerRows < 6
|
|
41
|
+
|
|
24
42
|
const w = definition.backgroundWidth * scale
|
|
25
43
|
const h = definition.backgroundHeight * scale
|
|
26
44
|
|
|
27
|
-
// Source dimensions from definition (e.g., 176x166) — clip to this region from texture
|
|
28
45
|
const srcW = definition.backgroundWidth
|
|
29
46
|
const srcH = definition.backgroundHeight
|
|
30
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
|
+
|
|
31
57
|
return (
|
|
32
58
|
<div
|
|
33
59
|
className="mc-inv-background"
|
|
@@ -41,40 +67,55 @@ export function InventoryBackground({
|
|
|
41
67
|
outlineOffset: 0,
|
|
42
68
|
}}
|
|
43
69
|
>
|
|
44
|
-
{
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
top: 0,
|
|
50
|
-
left: 0,
|
|
51
|
-
width: srcW,
|
|
52
|
-
height: srcH,
|
|
53
|
-
overflow: 'hidden',
|
|
54
|
-
transform: `scale(${scale})`,
|
|
55
|
-
transformOrigin: 'top left',
|
|
56
|
-
}}
|
|
57
|
-
>
|
|
58
|
-
{/* Background texture — render at natural size, clipped by wrapper overflow */}
|
|
59
|
-
<img
|
|
60
|
-
className="mc-inv-background-image"
|
|
61
|
-
src={bgUrl}
|
|
62
|
-
alt=""
|
|
63
|
-
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"
|
|
64
75
|
style={{
|
|
65
|
-
|
|
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,
|
|
66
102
|
width: srcW,
|
|
67
103
|
height: srcH,
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
// Clip to top-left srcW×srcH region (if texture is larger)
|
|
72
|
-
objectFit: 'none',
|
|
73
|
-
objectPosition: '0 0',
|
|
104
|
+
overflow: 'hidden',
|
|
105
|
+
transform: `scale(${scale})`,
|
|
106
|
+
transformOrigin: 'top left',
|
|
74
107
|
}}
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
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
|
+
)}
|
|
78
119
|
|
|
79
120
|
{/* Title */}
|
|
80
121
|
{title !== undefined && (
|
|
@@ -10,6 +10,8 @@ import { ProgressBar } from './ProgressBar'
|
|
|
10
10
|
import { VillagerTradeList } from './VillagerTradeList'
|
|
11
11
|
import { EnchantmentOptions } from './EnchantmentOptions'
|
|
12
12
|
import { HotbarExtras } from './HotbarExtras'
|
|
13
|
+
import { AnvilInput } from './AnvilInput'
|
|
14
|
+
import { EntityDisplay } from './EntityDisplay'
|
|
13
15
|
|
|
14
16
|
interface InventoryWindowProps {
|
|
15
17
|
type: string
|
|
@@ -20,6 +22,8 @@ interface InventoryWindowProps {
|
|
|
20
22
|
style?: React.CSSProperties
|
|
21
23
|
enableKeyboardShortcuts?: boolean
|
|
22
24
|
showDebug?: boolean
|
|
25
|
+
/** Override entity display rendering. Pass a function returning JSX, or null to hide. */
|
|
26
|
+
renderEntity?: ((width: number, height: number) => React.ReactNode) | null
|
|
23
27
|
}
|
|
24
28
|
|
|
25
29
|
export function InventoryWindow({
|
|
@@ -31,6 +35,7 @@ export function InventoryWindow({
|
|
|
31
35
|
style,
|
|
32
36
|
enableKeyboardShortcuts = true,
|
|
33
37
|
showDebug = false,
|
|
38
|
+
renderEntity,
|
|
34
39
|
}: InventoryWindowProps) {
|
|
35
40
|
const def = getInventoryType(type)
|
|
36
41
|
const { windowState, getSlot } = useInventoryContext()
|
|
@@ -62,6 +67,7 @@ export function InventoryWindow({
|
|
|
62
67
|
const isVillager = type === 'villager'
|
|
63
68
|
const isEnchanting = type === 'enchanting_table'
|
|
64
69
|
const isHotbar = type === 'hotbar'
|
|
70
|
+
const isAnvil = type === 'anvil'
|
|
65
71
|
|
|
66
72
|
const resolveItem = (slotIndex: number) => {
|
|
67
73
|
const fromProp = effectiveSlots.find((s) => s.index === slotIndex)
|
|
@@ -97,6 +103,11 @@ export function InventoryWindow({
|
|
|
97
103
|
</div>
|
|
98
104
|
))}
|
|
99
105
|
|
|
106
|
+
{/* Entity display area */}
|
|
107
|
+
{def.entityDisplay && (
|
|
108
|
+
<EntityDisplay area={def.entityDisplay} renderEntity={renderEntity} />
|
|
109
|
+
)}
|
|
110
|
+
|
|
100
111
|
{/* Progress bars */}
|
|
101
112
|
{def.progressBars?.map((pb) => (
|
|
102
113
|
<ProgressBar
|
|
@@ -119,6 +130,9 @@ export function InventoryWindow({
|
|
|
119
130
|
/>
|
|
120
131
|
)}
|
|
121
132
|
|
|
133
|
+
{/* Anvil rename input */}
|
|
134
|
+
{isAnvil && <AnvilInput x={59} y={20} width={110} height={16} />}
|
|
135
|
+
|
|
122
136
|
{/* Hotbar extras: active slot indicator, offhand slot, open-inventory button */}
|
|
123
137
|
{isHotbar && (
|
|
124
138
|
<HotbarExtras
|
|
@@ -7,8 +7,10 @@ import { useDataUrl, isTextureFailed } from '../../cache/textureCache'
|
|
|
7
7
|
interface ItemCanvasProps {
|
|
8
8
|
item: ItemStack
|
|
9
9
|
size?: number
|
|
10
|
-
|
|
11
|
-
|
|
10
|
+
/** Hide item count overlay */
|
|
11
|
+
noCount?: boolean
|
|
12
|
+
/** Hide durability bar */
|
|
13
|
+
noDurability?: boolean
|
|
12
14
|
className?: string
|
|
13
15
|
style?: React.CSSProperties
|
|
14
16
|
}
|
|
@@ -24,8 +26,8 @@ function getDurabilityColor(current: number, max: number): string {
|
|
|
24
26
|
export const ItemCanvas = memo(function ItemCanvas({
|
|
25
27
|
item,
|
|
26
28
|
size,
|
|
27
|
-
|
|
28
|
-
|
|
29
|
+
noCount = false,
|
|
30
|
+
noDurability = false,
|
|
29
31
|
className,
|
|
30
32
|
style,
|
|
31
33
|
}: ItemCanvasProps) {
|
|
@@ -34,7 +36,8 @@ export const ItemCanvas = memo(function ItemCanvas({
|
|
|
34
36
|
const renderSize = size ?? contentSize
|
|
35
37
|
|
|
36
38
|
const primaryUrl = textures.getItemTextureUrl(item)
|
|
37
|
-
|
|
39
|
+
// Skip the block-texture fallback when a textureKey override is already in use
|
|
40
|
+
const fallbackUrl = !item.textureKey && item.name ? textures.getBlockTextureUrl(item) : null
|
|
38
41
|
|
|
39
42
|
// Load primary URL as cached data URL
|
|
40
43
|
const primaryDataUrl = useDataUrl(primaryUrl)
|
|
@@ -54,7 +57,7 @@ export const ItemCanvas = memo(function ItemCanvas({
|
|
|
54
57
|
: primaryFailed && !fallbackUrl
|
|
55
58
|
|
|
56
59
|
const hasDurability =
|
|
57
|
-
|
|
60
|
+
!noDurability &&
|
|
58
61
|
item.durability !== undefined &&
|
|
59
62
|
item.maxDurability !== undefined &&
|
|
60
63
|
item.durability < item.maxDurability
|
|
@@ -129,7 +132,7 @@ export const ItemCanvas = memo(function ItemCanvas({
|
|
|
129
132
|
)}
|
|
130
133
|
|
|
131
134
|
{/* Item count */}
|
|
132
|
-
{
|
|
135
|
+
{!noCount && item.count > 1 && (
|
|
133
136
|
<span className="mc-inv-item-count" style={{
|
|
134
137
|
position: 'absolute',
|
|
135
138
|
bottom: 0,
|
|
@@ -143,6 +143,12 @@ export function JEI({
|
|
|
143
143
|
// F / R / U keyboard handlers
|
|
144
144
|
useEffect(() => {
|
|
145
145
|
const handler = (e: KeyboardEvent) => {
|
|
146
|
+
// Shift+F: toggle favorites-only filter
|
|
147
|
+
if (e.code === 'KeyF' && e.shiftKey) {
|
|
148
|
+
setShowFavorites((v) => !v)
|
|
149
|
+
setPage(0)
|
|
150
|
+
return
|
|
151
|
+
}
|
|
146
152
|
// F: toggle favorite for hovered JEI item
|
|
147
153
|
if (e.code === 'KeyF' && hoveredSlot !== null && hoveredSlot < 0) {
|
|
148
154
|
const item = slotToItemRef.current.get(hoveredSlot)
|
|
@@ -235,7 +241,7 @@ export function JEI({
|
|
|
235
241
|
style={{
|
|
236
242
|
padding: `${padding}px`,
|
|
237
243
|
background: '#c6c6c6',
|
|
238
|
-
border: `${scale}px solid #555555`,
|
|
244
|
+
// border: `${scale}px solid #555555`,
|
|
239
245
|
flexShrink: 0,
|
|
240
246
|
}}
|
|
241
247
|
>
|