minecraft-inventory 0.1.0
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 +566 -0
- package/package.json +37 -0
- package/src/InventoryGUI.tsx +108 -0
- package/src/cache/textureCache.ts +95 -0
- package/src/components/CursorItem/CursorItem.tsx +94 -0
- package/src/components/CursorItem/index.ts +1 -0
- package/src/components/HUD/HUD.tsx +11 -0
- package/src/components/HUD/index.ts +1 -0
- package/src/components/Hotbar/Hotbar.tsx +180 -0
- package/src/components/Hotbar/index.ts +1 -0
- package/src/components/InventoryOverlay/InventoryOverlay.tsx +196 -0
- package/src/components/InventoryOverlay/index.ts +2 -0
- package/src/components/InventoryWindow/EnchantmentOptions.tsx +109 -0
- package/src/components/InventoryWindow/InventoryBackground.tsx +110 -0
- package/src/components/InventoryWindow/InventoryWindow.tsx +120 -0
- package/src/components/InventoryWindow/ProgressBar.tsx +78 -0
- package/src/components/InventoryWindow/VillagerTradeList.tsx +136 -0
- package/src/components/InventoryWindow/index.ts +5 -0
- package/src/components/ItemCanvas/ItemCanvas.tsx +154 -0
- package/src/components/ItemCanvas/index.ts +1 -0
- package/src/components/JEI/JEI.module.css +37 -0
- package/src/components/JEI/JEI.tsx +303 -0
- package/src/components/JEI/index.ts +2 -0
- package/src/components/RecipeGuide/RecipeInventoryView.tsx +293 -0
- package/src/components/RecipeGuide/index.ts +1 -0
- package/src/components/Slot/Slot.module.css +111 -0
- package/src/components/Slot/Slot.tsx +363 -0
- package/src/components/Slot/index.ts +1 -0
- package/src/components/Text/MessageFormatted.css +5 -0
- package/src/components/Text/MessageFormatted.tsx +79 -0
- package/src/components/Text/MessageFormattedString.tsx +74 -0
- package/src/components/Text/chatUtils.ts +172 -0
- package/src/components/Tooltip/Tooltip.module.css +56 -0
- package/src/components/Tooltip/Tooltip.tsx +130 -0
- package/src/components/Tooltip/index.ts +1 -0
- package/src/connector/demo.ts +213 -0
- package/src/connector/index.ts +4 -0
- package/src/connector/mineflayer.ts +113 -0
- package/src/connector/types.ts +41 -0
- package/src/context/InventoryContext.tsx +157 -0
- package/src/context/ScaleContext.tsx +73 -0
- package/src/context/TextureContext.tsx +70 -0
- package/src/globals.d.ts +4 -0
- package/src/hooks/useKeyboardShortcuts.ts +41 -0
- package/src/hooks/useMobile.ts +28 -0
- package/src/index.tsx +65 -0
- package/src/mount.tsx +52 -0
- package/src/registry/index.ts +21 -0
- package/src/registry/inventories.ts +612 -0
- package/src/styles/tokens.css +47 -0
- package/src/types.ts +176 -0
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
import React from 'react'
|
|
2
|
+
import { useTextures } from '../../context/TextureContext'
|
|
3
|
+
import { useScale } from '../../context/ScaleContext'
|
|
4
|
+
import { MessageFormattedString } from '../Text/MessageFormattedString'
|
|
5
|
+
import type { InventoryTypeDefinition } from '../../registry'
|
|
6
|
+
|
|
7
|
+
interface InventoryBackgroundProps {
|
|
8
|
+
definition: InventoryTypeDefinition
|
|
9
|
+
children: React.ReactNode
|
|
10
|
+
title?: string
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export function InventoryBackground({
|
|
14
|
+
definition,
|
|
15
|
+
children,
|
|
16
|
+
title,
|
|
17
|
+
}: InventoryBackgroundProps) {
|
|
18
|
+
const textures = useTextures()
|
|
19
|
+
const { scale } = useScale()
|
|
20
|
+
|
|
21
|
+
const bgUrl = textures.getGuiTextureUrl(definition.backgroundTexture, definition.guiTextureVersion)
|
|
22
|
+
const w = definition.backgroundWidth * scale
|
|
23
|
+
const h = definition.backgroundHeight * scale
|
|
24
|
+
|
|
25
|
+
// Source dimensions from definition (e.g., 176x166) — clip to this region from texture
|
|
26
|
+
const srcW = definition.backgroundWidth
|
|
27
|
+
const srcH = definition.backgroundHeight
|
|
28
|
+
|
|
29
|
+
return (
|
|
30
|
+
<div
|
|
31
|
+
className="mc-inv-background"
|
|
32
|
+
style={{
|
|
33
|
+
position: 'relative',
|
|
34
|
+
width: w,
|
|
35
|
+
height: h,
|
|
36
|
+
imageRendering: 'pixelated',
|
|
37
|
+
display: 'inline-block',
|
|
38
|
+
outline: '1px solid rgba(255, 0, 0, 0.7)',
|
|
39
|
+
outlineOffset: 0,
|
|
40
|
+
}}
|
|
41
|
+
>
|
|
42
|
+
{/* Background texture wrapper — clips source to srcW×srcH, then scales */}
|
|
43
|
+
<div
|
|
44
|
+
className="mc-inv-background-wrapper"
|
|
45
|
+
style={{
|
|
46
|
+
position: 'absolute',
|
|
47
|
+
top: 0,
|
|
48
|
+
left: 0,
|
|
49
|
+
width: srcW,
|
|
50
|
+
height: srcH,
|
|
51
|
+
overflow: 'hidden',
|
|
52
|
+
transform: `scale(${scale})`,
|
|
53
|
+
transformOrigin: 'top left',
|
|
54
|
+
}}
|
|
55
|
+
>
|
|
56
|
+
{/* Background texture — render at natural size, clipped by wrapper overflow */}
|
|
57
|
+
<img
|
|
58
|
+
className="mc-inv-background-image"
|
|
59
|
+
src={bgUrl}
|
|
60
|
+
alt=""
|
|
61
|
+
aria-hidden
|
|
62
|
+
style={{
|
|
63
|
+
display: 'block',
|
|
64
|
+
width: srcW,
|
|
65
|
+
height: srcH,
|
|
66
|
+
imageRendering: 'pixelated',
|
|
67
|
+
pointerEvents: 'none',
|
|
68
|
+
userSelect: 'none',
|
|
69
|
+
// Clip to top-left srcW×srcH region (if texture is larger)
|
|
70
|
+
objectFit: 'none',
|
|
71
|
+
objectPosition: '0 0',
|
|
72
|
+
}}
|
|
73
|
+
draggable={false}
|
|
74
|
+
/>
|
|
75
|
+
</div>
|
|
76
|
+
|
|
77
|
+
{/* Title */}
|
|
78
|
+
{title !== undefined && (
|
|
79
|
+
<div
|
|
80
|
+
className="mc-inv-background-title"
|
|
81
|
+
style={{
|
|
82
|
+
position: 'absolute',
|
|
83
|
+
top: 6 * scale,
|
|
84
|
+
left: 8 * scale,
|
|
85
|
+
fontSize: 7 * scale,
|
|
86
|
+
fontFamily: "'Minecraftia', 'Minecraft', monospace",
|
|
87
|
+
pointerEvents: 'none',
|
|
88
|
+
lineHeight: 1,
|
|
89
|
+
}}
|
|
90
|
+
>
|
|
91
|
+
<MessageFormattedString text={title} fallbackColor="#404040" />
|
|
92
|
+
</div>
|
|
93
|
+
)}
|
|
94
|
+
|
|
95
|
+
{/* Slot layer */}
|
|
96
|
+
<div
|
|
97
|
+
className="mc-inv-background-slots"
|
|
98
|
+
style={{
|
|
99
|
+
position: 'absolute',
|
|
100
|
+
top: 0,
|
|
101
|
+
left: 0,
|
|
102
|
+
width: w,
|
|
103
|
+
height: h,
|
|
104
|
+
}}
|
|
105
|
+
>
|
|
106
|
+
{children}
|
|
107
|
+
</div>
|
|
108
|
+
</div>
|
|
109
|
+
)
|
|
110
|
+
}
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
import React, { useMemo } from 'react'
|
|
2
|
+
import type { SlotState } from '../../types'
|
|
3
|
+
import { getInventoryType } from '../../registry'
|
|
4
|
+
import { useInventoryContext } from '../../context/InventoryContext'
|
|
5
|
+
import { useScale } from '../../context/ScaleContext'
|
|
6
|
+
import { useKeyboardShortcuts } from '../../hooks/useKeyboardShortcuts'
|
|
7
|
+
import { Slot } from '../Slot'
|
|
8
|
+
import { InventoryBackground } from './InventoryBackground'
|
|
9
|
+
import { ProgressBar } from './ProgressBar'
|
|
10
|
+
import { VillagerTradeList } from './VillagerTradeList'
|
|
11
|
+
import { EnchantmentOptions } from './EnchantmentOptions'
|
|
12
|
+
|
|
13
|
+
interface InventoryWindowProps {
|
|
14
|
+
type: string
|
|
15
|
+
title?: string
|
|
16
|
+
slots?: SlotState[]
|
|
17
|
+
properties?: Record<string, number>
|
|
18
|
+
className?: string
|
|
19
|
+
style?: React.CSSProperties
|
|
20
|
+
enableKeyboardShortcuts?: boolean
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export function InventoryWindow({
|
|
24
|
+
type,
|
|
25
|
+
title,
|
|
26
|
+
slots: slotsProp,
|
|
27
|
+
properties = {},
|
|
28
|
+
className,
|
|
29
|
+
style,
|
|
30
|
+
enableKeyboardShortcuts = true,
|
|
31
|
+
}: InventoryWindowProps) {
|
|
32
|
+
const def = getInventoryType(type)
|
|
33
|
+
const { windowState, getSlot } = useInventoryContext()
|
|
34
|
+
const { scale, slotSize, contentSize } = useScale()
|
|
35
|
+
// borderPx removed from slot positioning: registry coords already point to the item area
|
|
36
|
+
const borderPx = 0
|
|
37
|
+
|
|
38
|
+
useKeyboardShortcuts(enableKeyboardShortcuts)
|
|
39
|
+
|
|
40
|
+
const effectiveSlots = useMemo(() => {
|
|
41
|
+
if (slotsProp) return slotsProp
|
|
42
|
+
return windowState?.slots ?? []
|
|
43
|
+
}, [slotsProp, windowState])
|
|
44
|
+
|
|
45
|
+
const effectiveProperties = useMemo(() => {
|
|
46
|
+
return { ...properties, ...(windowState?.properties ?? {}) }
|
|
47
|
+
}, [properties, windowState])
|
|
48
|
+
|
|
49
|
+
if (!def) {
|
|
50
|
+
return (
|
|
51
|
+
<div style={{ color: 'red', padding: 8 }}>
|
|
52
|
+
Unknown inventory type: {type}
|
|
53
|
+
</div>
|
|
54
|
+
)
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// Player inventory: don't show title (it's just the player's own inventory)
|
|
58
|
+
const effectiveTitle = type === 'player' ? undefined : (title ?? windowState?.title ?? def.title)
|
|
59
|
+
const isVillager = type === 'villager'
|
|
60
|
+
const isEnchanting = type === 'enchanting_table'
|
|
61
|
+
|
|
62
|
+
const resolveItem = (slotIndex: number) => {
|
|
63
|
+
const fromProp = effectiveSlots.find((s) => s.index === slotIndex)
|
|
64
|
+
if (fromProp) return fromProp.item
|
|
65
|
+
return getSlot(slotIndex)?.item ?? null
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
return (
|
|
69
|
+
<div
|
|
70
|
+
className={['mc-inv-root', className].filter(Boolean).join(' ')}
|
|
71
|
+
style={{ position: 'relative', display: 'inline-block', ...style }}
|
|
72
|
+
>
|
|
73
|
+
<InventoryBackground definition={def} title={effectiveTitle}>
|
|
74
|
+
{/* Render slots */}
|
|
75
|
+
{def.slots.map((slotDef) => (
|
|
76
|
+
<div
|
|
77
|
+
key={slotDef.index}
|
|
78
|
+
className="mc-inv-slot-wrapper"
|
|
79
|
+
style={{
|
|
80
|
+
position: 'absolute',
|
|
81
|
+
// Offset by borderPx to land inside the texture's slot cell (skip the 1px border)
|
|
82
|
+
left: slotDef.x * scale + borderPx,
|
|
83
|
+
top: slotDef.y * scale + borderPx,
|
|
84
|
+
}}
|
|
85
|
+
>
|
|
86
|
+
<Slot
|
|
87
|
+
index={slotDef.index}
|
|
88
|
+
item={resolveItem(slotDef.index)}
|
|
89
|
+
size={slotDef.size ? slotDef.size * scale - 2 * borderPx : contentSize}
|
|
90
|
+
resultSlot={slotDef.resultSlot}
|
|
91
|
+
label={slotDef.label}
|
|
92
|
+
/>
|
|
93
|
+
</div>
|
|
94
|
+
))}
|
|
95
|
+
|
|
96
|
+
{/* Progress bars */}
|
|
97
|
+
{def.progressBars?.map((pb) => (
|
|
98
|
+
<ProgressBar
|
|
99
|
+
key={pb.id}
|
|
100
|
+
definition={pb}
|
|
101
|
+
properties={effectiveProperties}
|
|
102
|
+
backgroundTexture={def.backgroundTexture}
|
|
103
|
+
/>
|
|
104
|
+
))}
|
|
105
|
+
|
|
106
|
+
{/* Villager trades */}
|
|
107
|
+
{isVillager && <VillagerTradeList />}
|
|
108
|
+
|
|
109
|
+
{/* Enchanting options */}
|
|
110
|
+
{isEnchanting && (
|
|
111
|
+
<EnchantmentOptions
|
|
112
|
+
properties={effectiveProperties}
|
|
113
|
+
x={60}
|
|
114
|
+
y={14}
|
|
115
|
+
/>
|
|
116
|
+
)}
|
|
117
|
+
</InventoryBackground>
|
|
118
|
+
</div>
|
|
119
|
+
)
|
|
120
|
+
}
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import React from 'react'
|
|
2
|
+
import { useTextures } from '../../context/TextureContext'
|
|
3
|
+
import { useScale } from '../../context/ScaleContext'
|
|
4
|
+
import type { ProgressBar as ProgressBarDef } from '../../types'
|
|
5
|
+
|
|
6
|
+
interface ProgressBarProps {
|
|
7
|
+
definition: ProgressBarDef
|
|
8
|
+
properties: Record<string, number>
|
|
9
|
+
backgroundTexture: string
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export function ProgressBar({ definition, properties, backgroundTexture }: ProgressBarProps) {
|
|
13
|
+
const textures = useTextures()
|
|
14
|
+
const { scale } = useScale()
|
|
15
|
+
|
|
16
|
+
const value = definition.getValue(properties)
|
|
17
|
+
const max = definition.getMax(properties)
|
|
18
|
+
const ratio = max > 0 ? Math.min(1, Math.max(0, value / max)) : 0
|
|
19
|
+
|
|
20
|
+
const bgUrl = textures.getGuiTextureUrl(backgroundTexture)
|
|
21
|
+
const x = definition.x * scale
|
|
22
|
+
const y = definition.y * scale
|
|
23
|
+
const w = definition.width * scale
|
|
24
|
+
const h = definition.height * scale
|
|
25
|
+
const srcX = definition.textureX
|
|
26
|
+
const srcY = definition.textureY
|
|
27
|
+
|
|
28
|
+
let clipPath: string
|
|
29
|
+
let clipWidth = w
|
|
30
|
+
let clipHeight = h
|
|
31
|
+
|
|
32
|
+
switch (definition.direction) {
|
|
33
|
+
case 'right':
|
|
34
|
+
clipWidth = w * ratio
|
|
35
|
+
break
|
|
36
|
+
case 'left':
|
|
37
|
+
clipWidth = w * ratio
|
|
38
|
+
break
|
|
39
|
+
case 'up':
|
|
40
|
+
clipHeight = h * ratio
|
|
41
|
+
break
|
|
42
|
+
case 'down':
|
|
43
|
+
clipHeight = h * ratio
|
|
44
|
+
break
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
if (ratio <= 0) return null
|
|
48
|
+
|
|
49
|
+
return (
|
|
50
|
+
<div
|
|
51
|
+
style={{
|
|
52
|
+
position: 'absolute',
|
|
53
|
+
left: x,
|
|
54
|
+
top: y,
|
|
55
|
+
width: clipWidth,
|
|
56
|
+
height: clipHeight,
|
|
57
|
+
overflow: 'hidden',
|
|
58
|
+
imageRendering: 'pixelated',
|
|
59
|
+
pointerEvents: 'none',
|
|
60
|
+
}}
|
|
61
|
+
>
|
|
62
|
+
<img
|
|
63
|
+
src={bgUrl}
|
|
64
|
+
alt=""
|
|
65
|
+
aria-hidden
|
|
66
|
+
style={{
|
|
67
|
+
position: 'absolute',
|
|
68
|
+
left: -srcX * scale,
|
|
69
|
+
top: -srcY * scale,
|
|
70
|
+
imageRendering: 'pixelated',
|
|
71
|
+
display: 'block',
|
|
72
|
+
maxWidth: 'none',
|
|
73
|
+
}}
|
|
74
|
+
draggable={false}
|
|
75
|
+
/>
|
|
76
|
+
</div>
|
|
77
|
+
)
|
|
78
|
+
}
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
import React, { useState } from 'react'
|
|
2
|
+
import type { TradeOffer } from '../../types'
|
|
3
|
+
import { useInventoryContext } from '../../context/InventoryContext'
|
|
4
|
+
import { useScale } from '../../context/ScaleContext'
|
|
5
|
+
import { ItemCanvas } from '../ItemCanvas'
|
|
6
|
+
import { Tooltip } from '../Tooltip'
|
|
7
|
+
|
|
8
|
+
interface TradeRowProps {
|
|
9
|
+
trade: TradeOffer
|
|
10
|
+
index: number
|
|
11
|
+
selected: boolean
|
|
12
|
+
onSelect(): void
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function TradeRow({ trade, index, selected, onSelect }: TradeRowProps) {
|
|
16
|
+
const { scale } = useScale()
|
|
17
|
+
const { sendAction } = useInventoryContext()
|
|
18
|
+
const [hoveredItem, setHoveredItem] = useState<'in1' | 'in2' | 'out' | null>(null)
|
|
19
|
+
const slotSize = 16 * scale
|
|
20
|
+
|
|
21
|
+
const handleClick = () => {
|
|
22
|
+
onSelect()
|
|
23
|
+
sendAction({ type: 'trade', tradeIndex: index })
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
return (
|
|
27
|
+
<div
|
|
28
|
+
onClick={handleClick}
|
|
29
|
+
style={{
|
|
30
|
+
display: 'flex',
|
|
31
|
+
alignItems: 'center',
|
|
32
|
+
gap: scale * 2,
|
|
33
|
+
padding: `${scale * 2}px`,
|
|
34
|
+
background: selected ? '#555555' : trade.disabled ? '#222222' : '#404040',
|
|
35
|
+
border: `${scale}px solid ${selected ? '#aaaaaa' : '#666666'}`,
|
|
36
|
+
cursor: trade.disabled ? 'not-allowed' : 'pointer',
|
|
37
|
+
opacity: trade.disabled ? 0.6 : 1,
|
|
38
|
+
width: '100%',
|
|
39
|
+
boxSizing: 'border-box',
|
|
40
|
+
}}
|
|
41
|
+
>
|
|
42
|
+
{/* Input 1 */}
|
|
43
|
+
<div
|
|
44
|
+
style={{ position: 'relative' }}
|
|
45
|
+
onMouseEnter={() => setHoveredItem('in1')}
|
|
46
|
+
onMouseLeave={() => setHoveredItem(null)}
|
|
47
|
+
>
|
|
48
|
+
<ItemCanvas item={trade.inputItem1} size={slotSize} />
|
|
49
|
+
{hoveredItem === 'in1' && (
|
|
50
|
+
<Tooltip item={trade.inputItem1} visible />
|
|
51
|
+
)}
|
|
52
|
+
</div>
|
|
53
|
+
|
|
54
|
+
{/* Input 2 */}
|
|
55
|
+
{trade.inputItem2 && (
|
|
56
|
+
<div
|
|
57
|
+
style={{ position: 'relative' }}
|
|
58
|
+
onMouseEnter={() => setHoveredItem('in2')}
|
|
59
|
+
onMouseLeave={() => setHoveredItem(null)}
|
|
60
|
+
>
|
|
61
|
+
<ItemCanvas item={trade.inputItem2} size={slotSize} />
|
|
62
|
+
{hoveredItem === 'in2' && (
|
|
63
|
+
<Tooltip item={trade.inputItem2} visible />
|
|
64
|
+
)}
|
|
65
|
+
</div>
|
|
66
|
+
)}
|
|
67
|
+
|
|
68
|
+
{/* Arrow */}
|
|
69
|
+
<div style={{ color: '#aaaaaa', fontSize: scale * 8 }}>→</div>
|
|
70
|
+
|
|
71
|
+
{/* Output */}
|
|
72
|
+
<div
|
|
73
|
+
style={{ position: 'relative' }}
|
|
74
|
+
onMouseEnter={() => setHoveredItem('out')}
|
|
75
|
+
onMouseLeave={() => setHoveredItem(null)}
|
|
76
|
+
>
|
|
77
|
+
<ItemCanvas item={trade.outputItem} size={slotSize} />
|
|
78
|
+
{hoveredItem === 'out' && (
|
|
79
|
+
<Tooltip item={trade.outputItem} visible />
|
|
80
|
+
)}
|
|
81
|
+
</div>
|
|
82
|
+
|
|
83
|
+
{/* Uses */}
|
|
84
|
+
<div
|
|
85
|
+
style={{
|
|
86
|
+
marginLeft: 'auto',
|
|
87
|
+
fontSize: scale * 6,
|
|
88
|
+
color: trade.uses >= trade.maxUses ? '#ff5555' : '#aaaaaa',
|
|
89
|
+
whiteSpace: 'nowrap',
|
|
90
|
+
}}
|
|
91
|
+
>
|
|
92
|
+
{trade.uses}/{trade.maxUses}
|
|
93
|
+
</div>
|
|
94
|
+
</div>
|
|
95
|
+
)
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
export function VillagerTradeList() {
|
|
99
|
+
const { windowState } = useInventoryContext()
|
|
100
|
+
const { scale } = useScale()
|
|
101
|
+
const [selectedIndex, setSelectedIndex] = useState(0)
|
|
102
|
+
|
|
103
|
+
const villagerState = windowState as (typeof windowState & {
|
|
104
|
+
trades?: TradeOffer[]
|
|
105
|
+
}) | null
|
|
106
|
+
|
|
107
|
+
const trades = villagerState?.trades ?? []
|
|
108
|
+
if (trades.length === 0) return null
|
|
109
|
+
|
|
110
|
+
return (
|
|
111
|
+
<div
|
|
112
|
+
style={{
|
|
113
|
+
position: 'absolute',
|
|
114
|
+
left: 4 * scale,
|
|
115
|
+
top: 18 * scale,
|
|
116
|
+
width: 100 * scale,
|
|
117
|
+
maxHeight: 130 * scale,
|
|
118
|
+
overflowY: 'auto',
|
|
119
|
+
display: 'flex',
|
|
120
|
+
flexDirection: 'column',
|
|
121
|
+
gap: scale,
|
|
122
|
+
scrollbarWidth: 'thin',
|
|
123
|
+
}}
|
|
124
|
+
>
|
|
125
|
+
{trades.map((trade, i) => (
|
|
126
|
+
<TradeRow
|
|
127
|
+
key={i}
|
|
128
|
+
trade={trade}
|
|
129
|
+
index={i}
|
|
130
|
+
selected={selectedIndex === i}
|
|
131
|
+
onSelect={() => setSelectedIndex(i)}
|
|
132
|
+
/>
|
|
133
|
+
))}
|
|
134
|
+
</div>
|
|
135
|
+
)
|
|
136
|
+
}
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
export { InventoryWindow } from './InventoryWindow'
|
|
2
|
+
export { InventoryBackground } from './InventoryBackground'
|
|
3
|
+
export { ProgressBar } from './ProgressBar'
|
|
4
|
+
export { VillagerTradeList } from './VillagerTradeList'
|
|
5
|
+
export { EnchantmentOptions } from './EnchantmentOptions'
|
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
import React, { useState, useEffect, memo } from 'react'
|
|
2
|
+
import type { ItemStack } from '../../types'
|
|
3
|
+
import { useTextures } from '../../context/TextureContext'
|
|
4
|
+
import { useScale } from '../../context/ScaleContext'
|
|
5
|
+
import { useDataUrl, isTextureFailed } from '../../cache/textureCache'
|
|
6
|
+
|
|
7
|
+
interface ItemCanvasProps {
|
|
8
|
+
item: ItemStack
|
|
9
|
+
size?: number
|
|
10
|
+
showCount?: boolean
|
|
11
|
+
showDurability?: boolean
|
|
12
|
+
className?: string
|
|
13
|
+
style?: React.CSSProperties
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function getDurabilityColor(current: number, max: number): string {
|
|
17
|
+
const ratio = current / max
|
|
18
|
+
if (ratio > 0.6) return '#55ff55'
|
|
19
|
+
if (ratio > 0.3) return '#ffff55'
|
|
20
|
+
return '#ff5555'
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/** Renders a single item: texture as <img>, count as <span>, durability as CSS bars. */
|
|
24
|
+
export const ItemCanvas = memo(function ItemCanvas({
|
|
25
|
+
item,
|
|
26
|
+
size,
|
|
27
|
+
showCount = true,
|
|
28
|
+
showDurability = true,
|
|
29
|
+
className,
|
|
30
|
+
style,
|
|
31
|
+
}: ItemCanvasProps) {
|
|
32
|
+
const textures = useTextures()
|
|
33
|
+
const { contentSize, pixelSize } = useScale()
|
|
34
|
+
const renderSize = size ?? contentSize
|
|
35
|
+
|
|
36
|
+
const primaryUrl = textures.getItemTextureUrl(item)
|
|
37
|
+
const fallbackUrl = item.name ? textures.getBlockTextureUrl(item) : null
|
|
38
|
+
|
|
39
|
+
// Load primary URL as cached data URL
|
|
40
|
+
const primaryDataUrl = useDataUrl(primaryUrl)
|
|
41
|
+
const primaryFailed = primaryDataUrl === null || isTextureFailed(primaryUrl)
|
|
42
|
+
|
|
43
|
+
// Load fallback only once primary is known to have failed
|
|
44
|
+
const [loadFallback, setLoadFallback] = useState(false)
|
|
45
|
+
useEffect(() => {
|
|
46
|
+
if (primaryFailed && fallbackUrl) setLoadFallback(true)
|
|
47
|
+
else setLoadFallback(false)
|
|
48
|
+
}, [primaryFailed, fallbackUrl])
|
|
49
|
+
const fallbackDataUrl = useDataUrl(loadFallback ? fallbackUrl : null)
|
|
50
|
+
|
|
51
|
+
const src = loadFallback ? fallbackDataUrl : primaryDataUrl
|
|
52
|
+
const failed = loadFallback
|
|
53
|
+
? (fallbackDataUrl === null || isTextureFailed(fallbackUrl ?? ''))
|
|
54
|
+
: primaryFailed && !fallbackUrl
|
|
55
|
+
|
|
56
|
+
const hasDurability =
|
|
57
|
+
showDurability &&
|
|
58
|
+
item.durability !== undefined &&
|
|
59
|
+
item.maxDurability !== undefined &&
|
|
60
|
+
item.durability < item.maxDurability
|
|
61
|
+
|
|
62
|
+
const barColor = hasDurability
|
|
63
|
+
? getDurabilityColor(item.durability!, item.maxDurability!)
|
|
64
|
+
: '#55ff55'
|
|
65
|
+
|
|
66
|
+
// Font size for count: ~38% of slot size, min 7px, shadow offset = 1 scaled pixel
|
|
67
|
+
const countFontSize = Math.max(7, Math.round(renderSize * 0.38))
|
|
68
|
+
const shadow = Math.max(1, Math.round(pixelSize))
|
|
69
|
+
|
|
70
|
+
return (
|
|
71
|
+
<div
|
|
72
|
+
className={['mc-inv-item', className].filter(Boolean).join(' ')}
|
|
73
|
+
style={{
|
|
74
|
+
position: 'relative',
|
|
75
|
+
width: renderSize,
|
|
76
|
+
height: renderSize,
|
|
77
|
+
flexShrink: 0,
|
|
78
|
+
...style,
|
|
79
|
+
}}
|
|
80
|
+
>
|
|
81
|
+
{failed ? (
|
|
82
|
+
/* Missing texture — magenta/black checkerboard */
|
|
83
|
+
<div className="mc-inv-item-placeholder" style={{
|
|
84
|
+
width: '100%',
|
|
85
|
+
height: '100%',
|
|
86
|
+
background: 'linear-gradient(45deg, #8b008b 25%, #000 25%, #000 50%, #8b008b 50%, #8b008b 75%, #000 75%)',
|
|
87
|
+
backgroundSize: `${renderSize / 2}px ${renderSize / 2}px`,
|
|
88
|
+
}} />
|
|
89
|
+
) : src ? (
|
|
90
|
+
/* Loaded — render cached data URL; no onError needed */
|
|
91
|
+
<img
|
|
92
|
+
className="mc-inv-item-image"
|
|
93
|
+
src={src}
|
|
94
|
+
alt=""
|
|
95
|
+
aria-hidden
|
|
96
|
+
draggable={false}
|
|
97
|
+
style={{
|
|
98
|
+
width: '100%',
|
|
99
|
+
height: '100%',
|
|
100
|
+
imageRendering: 'pixelated',
|
|
101
|
+
display: 'block',
|
|
102
|
+
pointerEvents: 'none',
|
|
103
|
+
}}
|
|
104
|
+
/>
|
|
105
|
+
) : null /* still loading — render nothing (slot stays empty) */}
|
|
106
|
+
|
|
107
|
+
{/* Durability bar */}
|
|
108
|
+
{hasDurability && (
|
|
109
|
+
<>
|
|
110
|
+
<div className="mc-inv-item-durability-bg" style={{
|
|
111
|
+
position: 'absolute',
|
|
112
|
+
bottom: 1,
|
|
113
|
+
left: 1,
|
|
114
|
+
width: 'calc(100% - 2px)',
|
|
115
|
+
height: Math.max(1, Math.round(renderSize * 0.063)),
|
|
116
|
+
background: '#000',
|
|
117
|
+
pointerEvents: 'none',
|
|
118
|
+
}} />
|
|
119
|
+
<div className="mc-inv-item-durability-bar" style={{
|
|
120
|
+
position: 'absolute',
|
|
121
|
+
bottom: 1,
|
|
122
|
+
left: 1,
|
|
123
|
+
width: `${(item.durability! / item.maxDurability!) * 100}%`,
|
|
124
|
+
height: Math.max(1, Math.round(renderSize * 0.063)),
|
|
125
|
+
background: barColor,
|
|
126
|
+
pointerEvents: 'none',
|
|
127
|
+
}} />
|
|
128
|
+
</>
|
|
129
|
+
)}
|
|
130
|
+
|
|
131
|
+
{/* Item count */}
|
|
132
|
+
{showCount && item.count > 1 && (
|
|
133
|
+
<span className="mc-inv-item-count" style={{
|
|
134
|
+
position: 'absolute',
|
|
135
|
+
bottom: 0,
|
|
136
|
+
right: 0,
|
|
137
|
+
fontSize: countFontSize,
|
|
138
|
+
fontFamily: "'Minecraftia', 'Minecraft', monospace",
|
|
139
|
+
fontWeight: 'bold',
|
|
140
|
+
color: '#ffffff',
|
|
141
|
+
lineHeight: 1,
|
|
142
|
+
pointerEvents: 'none',
|
|
143
|
+
// Double shadow trick: dark shadow at +1,+1 for Minecraft count text look
|
|
144
|
+
textShadow: `${shadow}px ${shadow}px 0 rgba(0,0,0,0.6)`,
|
|
145
|
+
userSelect: 'none',
|
|
146
|
+
// Prevent wrapping
|
|
147
|
+
whiteSpace: 'nowrap',
|
|
148
|
+
}}>
|
|
149
|
+
{item.count}
|
|
150
|
+
</span>
|
|
151
|
+
)}
|
|
152
|
+
</div>
|
|
153
|
+
)
|
|
154
|
+
})
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { ItemCanvas } from './ItemCanvas'
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
.jei {
|
|
2
|
+
font-family: 'Minecraft', monospace;
|
|
3
|
+
image-rendering: pixelated;
|
|
4
|
+
}
|
|
5
|
+
|
|
6
|
+
.searchInput::placeholder {
|
|
7
|
+
color: rgba(255, 255, 255, 0.5);
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
.pageBtn {
|
|
11
|
+
background: #404040;
|
|
12
|
+
color: #ffffff;
|
|
13
|
+
border: 1px solid #666666;
|
|
14
|
+
cursor: pointer;
|
|
15
|
+
font-family: inherit;
|
|
16
|
+
font-size: inherit;
|
|
17
|
+
transition: background 0.1s;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
.pageBtn:hover:not(:disabled) {
|
|
21
|
+
background: #555555;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
.pageBtn:disabled {
|
|
25
|
+
opacity: 0.4;
|
|
26
|
+
cursor: not-allowed;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
.pageBtnActive {
|
|
30
|
+
background: #8b6914;
|
|
31
|
+
color: #ffcc00;
|
|
32
|
+
border-color: #ffcc00;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
.pageBtnActive:hover:not(:disabled) {
|
|
36
|
+
background: #a07820;
|
|
37
|
+
}
|