minecraft-inventory 0.1.3 → 0.1.4
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 +56 -2
- 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/Slot/Slot.tsx +108 -11
- package/src/connector/mineflayer.ts +80 -25
- package/src/context/InventoryContext.tsx +143 -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 +4 -0
- package/src/registry/inventories.ts +98 -6
- package/src/styles/tokens.css +6 -0
- package/src/types.ts +24 -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.4",
|
|
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
|
+
}
|
|
@@ -1,9 +1,55 @@
|
|
|
1
|
-
import React from 'react'
|
|
1
|
+
import React, { useEffect, useState } 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
|
+
/**
|
|
8
|
+
* For generic_9xN (N < 6): canvas-stitch the 6-row generic_54 texture.
|
|
9
|
+
* Takes the top (title + N rows) and bottom (player-inventory section = last 96px)
|
|
10
|
+
* from the source and composes them into a data URL of the correct output height.
|
|
11
|
+
*
|
|
12
|
+
* The source texture (generic_54.png, 176×222) layout:
|
|
13
|
+
* y=0..16 — title bar (17px)
|
|
14
|
+
* y=17..124 — 6 container rows (6×18 = 108px)
|
|
15
|
+
* y=126..221 — player inventory section (96px)
|
|
16
|
+
*/
|
|
17
|
+
function useStitchedTexture(srcUrl: string, containerRows: number): string | null {
|
|
18
|
+
const [dataUrl, setDataUrl] = useState<string | null>(null)
|
|
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
|
+
}
|
|
52
|
+
|
|
7
53
|
interface InventoryBackgroundProps {
|
|
8
54
|
definition: InventoryTypeDefinition
|
|
9
55
|
children: React.ReactNode
|
|
@@ -20,7 +66,13 @@ export function InventoryBackground({
|
|
|
20
66
|
const textures = useTextures()
|
|
21
67
|
const { scale } = useScale()
|
|
22
68
|
|
|
23
|
-
const
|
|
69
|
+
const rawBgUrl = textures.getGuiTextureUrl(definition.backgroundTexture)
|
|
70
|
+
// For generic_9xN (containerRows defined and < 6): canvas-stitch the texture
|
|
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
|
|
75
|
+
|
|
24
76
|
const w = definition.backgroundWidth * scale
|
|
25
77
|
const h = definition.backgroundHeight * scale
|
|
26
78
|
|
|
@@ -56,6 +108,7 @@ export function InventoryBackground({
|
|
|
56
108
|
}}
|
|
57
109
|
>
|
|
58
110
|
{/* Background texture — render at natural size, clipped by wrapper overflow */}
|
|
111
|
+
{bgUrl && (
|
|
59
112
|
<img
|
|
60
113
|
className="mc-inv-background-image"
|
|
61
114
|
src={bgUrl}
|
|
@@ -74,6 +127,7 @@ export function InventoryBackground({
|
|
|
74
127
|
}}
|
|
75
128
|
draggable={false}
|
|
76
129
|
/>
|
|
130
|
+
)}
|
|
77
131
|
</div>
|
|
78
132
|
|
|
79
133
|
{/* Title */}
|
|
@@ -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
|
>
|