minecraft-inventory 0.1.19 → 0.1.21

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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "minecraft-inventory",
3
- "version": "0.1.19",
3
+ "version": "0.1.21",
4
4
  "type": "module",
5
5
  "license": "MIT",
6
6
  "release": {
@@ -1,4 +1,5 @@
1
1
  import React, { useCallback, useEffect, useRef } from 'react'
2
+ import { useCloseOnWClick } from './hooks/useCloseOnWClick'
2
3
  import type { InventoryConnector } from './connector/types'
3
4
  import type { SlotState } from './types'
4
5
  import { InventoryProvider } from './context/InventoryContext'
@@ -51,6 +52,8 @@ export function InventoryGUI({
51
52
  }: InventoryGUIProps) {
52
53
  const containerRef = useRef<HTMLDivElement>(null)
53
54
 
55
+ useCloseOnWClick(onClose)
56
+
54
57
  const handleClose = useCallback(
55
58
  (e: KeyboardEvent) => {
56
59
  if (e.code === 'Escape') onClose?.()
@@ -1,4 +1,5 @@
1
1
  import React, { useCallback, useState } from 'react'
2
+ import { useCloseOnWClick } from '../../hooks/useCloseOnWClick'
2
3
  import { useInventoryContext } from '../../context/InventoryContext'
3
4
  import { useScale } from '../../context/ScaleContext'
4
5
  import { getInventoryType } from '../../registry'
@@ -93,6 +94,8 @@ export function InventoryOverlay({
93
94
  const { heldItem, sendAction, setHeldItem, focusedSlot, setFocusedSlot } = useInventoryContext()
94
95
  const { scale } = useScale()
95
96
 
97
+ useCloseOnWClick(onClose)
98
+
96
99
  const def = getInventoryType(type)
97
100
  const invUnscaledW = def?.backgroundWidth ?? 176
98
101
  const sideGapPx = 5 * scale // gap from overlay edge and from inventory edge
@@ -196,7 +199,6 @@ export function InventoryOverlay({
196
199
  {(leftPanel || enableNotes || (showJEI && jeiPosition === 'left')) && (
197
200
  <div
198
201
  className="mc-inv-overlay-side mc-inv-overlay-side-left"
199
- onClick={(e) => e.stopPropagation()}
200
202
  style={{ ...sidePanelBase, left: sideGapPx, pointerEvents: 'auto' }}
201
203
  >
202
204
  {enableNotes && (
@@ -205,13 +207,17 @@ export function InventoryOverlay({
205
207
  </div>
206
208
  )}
207
209
  {leftPanel && (
208
- <div style={{ flex: 1, minHeight: 0, display: 'flex', flexDirection: 'column' }}>
210
+ <div
211
+ onClick={(e) => e.stopPropagation()}
212
+ style={{ flex: 1, minHeight: 0, display: 'flex', flexDirection: 'column' }}
213
+ >
209
214
  {leftPanel}
210
215
  </div>
211
216
  )}
212
217
  {showJEI && jeiPosition === 'left' && (
213
218
  <div
214
219
  className="mc-inv-overlay-jei mc-inv-overlay-jei-left"
220
+ onClick={(e) => e.stopPropagation()}
215
221
  style={{ flex: 1, minHeight: 0, display: 'flex', flexDirection: 'column', width: '100%' }}
216
222
  >
217
223
  {jeiPanel}
@@ -304,7 +310,7 @@ export function InventoryOverlay({
304
310
  lineHeight: 1,
305
311
  }}
306
312
  >
307
- INV 0.1.19
313
+ INV 0.1.21
308
314
  </a>
309
315
  )}
310
316
 
@@ -0,0 +1,46 @@
1
+ import React from 'react'
2
+ import { useScale } from '../../context/ScaleContext'
3
+ import { useInventoryContext } from '../../context/InventoryContext'
4
+
5
+ interface AnvilCostProps {
6
+ properties: Record<string, number>
7
+ backgroundWidth: number
8
+ }
9
+
10
+ export function AnvilCost({ properties, backgroundWidth }: AnvilCostProps) {
11
+ const { scale } = useScale()
12
+ const { windowState } = useInventoryContext()
13
+
14
+ const cost = properties.repairCost ?? 0
15
+ if (cost <= 0) return null
16
+
17
+ const hasResult = windowState?.slots.some((s) => s.index === 2 && s.item !== null) ?? false
18
+ if (!hasResult) return null
19
+
20
+ const tooExpensive = cost >= 40
21
+ const label = tooExpensive ? 'Too Expensive!' : `Enchantment Cost: ${cost}`
22
+ const color = tooExpensive ? '#FF6060' : '#80FF20'
23
+
24
+ return (
25
+ <div
26
+ style={{
27
+ position: 'absolute',
28
+ right: 8 * scale,
29
+ top: 67 * scale,
30
+ height: 12 * scale,
31
+ display: 'flex',
32
+ alignItems: 'center',
33
+ justifyContent: 'flex-end',
34
+ padding: `0 ${2 * scale}px`,
35
+ background: 'rgba(0, 0, 0, 0.3)',
36
+ fontSize: 7 * scale,
37
+ fontFamily: "'Minecraftia', 'Minecraft', monospace",
38
+ color,
39
+ whiteSpace: 'nowrap',
40
+ pointerEvents: 'none',
41
+ }}
42
+ >
43
+ {label}
44
+ </div>
45
+ )
46
+ }
@@ -42,6 +42,23 @@ const ENCHANTMENT_NAMES: Record<number, string> = {
42
42
  74: 'Vanishing Curse',
43
43
  }
44
44
 
45
+ const ROMAN_NUMERALS: [number, string][] = [
46
+ [10, 'X'], [9, 'IX'], [5, 'V'], [4, 'IV'], [1, 'I'],
47
+ ]
48
+
49
+ function toRoman(num: number): string {
50
+ if (num <= 0) return ''
51
+ let result = ''
52
+ let remaining = num
53
+ for (const [value, symbol] of ROMAN_NUMERALS) {
54
+ while (remaining >= value) {
55
+ result += symbol
56
+ remaining -= value
57
+ }
58
+ }
59
+ return result
60
+ }
61
+
45
62
  interface EnchantmentOptionsProps {
46
63
  properties: Record<string, number>
47
64
  x: number
@@ -49,9 +66,9 @@ interface EnchantmentOptionsProps {
49
66
  }
50
67
 
51
68
  const OPTION_KEYS = [
52
- { levelKey: 'topEnchantLevel', idKey: 'topEnchantId', slot: 0 },
53
- { levelKey: 'middleEnchantLevel', idKey: 'middleEnchantId', slot: 1 },
54
- { levelKey: 'bottomEnchantLevel', idKey: 'bottomEnchantId', slot: 2 },
69
+ { levelKey: 'topEnchantLevel', idKey: 'topEnchantId', clueKey: 'topLevelClue', slot: 0 },
70
+ { levelKey: 'middleEnchantLevel', idKey: 'middleEnchantId', clueKey: 'middleLevelClue', slot: 1 },
71
+ { levelKey: 'bottomEnchantLevel', idKey: 'bottomEnchantId', clueKey: 'bottomLevelClue', slot: 2 },
55
72
  ]
56
73
 
57
74
  export function EnchantmentOptions({ properties, x, y }: EnchantmentOptionsProps) {
@@ -69,11 +86,13 @@ export function EnchantmentOptions({ properties, x, y }: EnchantmentOptionsProps
69
86
  gap: 0,
70
87
  }}
71
88
  >
72
- {OPTION_KEYS.map(({ levelKey, idKey, slot }, i) => {
89
+ {OPTION_KEYS.map(({ levelKey, idKey, clueKey, slot }, i) => {
73
90
  const level = properties[levelKey] ?? 0
74
91
  const enchId = properties[idKey] ?? -1
92
+ const levelClue = properties[clueKey] ?? 0
75
93
  const name = ENCHANTMENT_NAMES[enchId] ?? (enchId >= 0 ? `Enchant #${enchId}` : '???')
76
94
  const disabled = level <= 0
95
+ const levelSuffix = levelClue > 0 ? ` ${toRoman(levelClue)}` : ''
77
96
 
78
97
  return (
79
98
  <div
@@ -99,7 +118,7 @@ export function EnchantmentOptions({ properties, x, y }: EnchantmentOptionsProps
99
118
  {level}
100
119
  </span>
101
120
  <span style={{ flex: 1, overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }}>
102
- {disabled ? '' : name}
121
+ {disabled ? '' : `${name}${levelSuffix}`}
103
122
  </span>
104
123
  </div>
105
124
  )
@@ -11,6 +11,7 @@ import { VillagerTradeList } from './VillagerTradeList'
11
11
  import { EnchantmentOptions } from './EnchantmentOptions'
12
12
  import { HotbarExtras } from './HotbarExtras'
13
13
  import { AnvilInput } from './AnvilInput'
14
+ import { AnvilCost } from './AnvilCost'
14
15
  import { EntityDisplay } from './EntityDisplay'
15
16
 
16
17
  interface InventoryWindowProps {
@@ -68,9 +69,9 @@ export function InventoryWindow({
68
69
  // Player inventory: don't show title (it's just the player's own inventory)
69
70
  const effectiveTitle = type === 'player' ? undefined : (title ?? windowState?.title ?? def.title)
70
71
  const isVillager = type === 'villager'
71
- const isEnchanting = type === 'enchanting_table'
72
+ const isEnchanting = def?.name === 'enchanting_table'
72
73
  const isHotbar = type === 'hotbar'
73
- const isAnvil = type === 'anvil'
74
+ const isAnvil = def?.name === 'anvil'
74
75
 
75
76
  const resolveItem = (slotIndex: number) => {
76
77
  const fromProp = effectiveSlots.find((s) => s.index === slotIndex)
@@ -139,8 +140,9 @@ export function InventoryWindow({
139
140
  />
140
141
  )}
141
142
 
142
- {/* Anvil rename input */}
143
+ {/* Anvil rename input + cost display */}
143
144
  {isAnvil && <AnvilInput x={59} y={20} width={110} height={16} />}
145
+ {isAnvil && <AnvilCost properties={effectiveProperties} backgroundWidth={def.backgroundWidth} />}
144
146
 
145
147
  {/* Hotbar extras: active slot indicator, offhand slot, open-inventory button */}
146
148
  {isHotbar && (
@@ -3,6 +3,7 @@ import type { ItemStack, RecipeGuide, RecipeNavFrame, BlockTextureRender } from
3
3
  import { useScale } from '../../context/ScaleContext'
4
4
  import { useInventoryContext } from '../../context/InventoryContext'
5
5
  import { Slot } from '../Slot'
6
+ import { StarIcon } from './StarIcon'
6
7
  import styles from './JEI.module.css'
7
8
 
8
9
  export interface JEIItem {
@@ -303,7 +304,7 @@ export function JEI({
303
304
  style={{ fontSize: 6 * scale, padding: `${scale}px ${2 * scale}px` }}
304
305
  title={showFavorites ? 'Show all items' : 'Show favorites (F to toggle)'}
305
306
  >
306
-
307
+ <StarIcon size={Math.max(1, Math.round(6 * scale))} />
307
308
  </button>
308
309
  </div>
309
310
  </div>
@@ -371,14 +372,13 @@ export function JEI({
371
372
  position: 'absolute',
372
373
  top: 0,
373
374
  right: 0,
374
- fontSize: Math.round(5 * scale),
375
- color: '#ffcc00',
376
- lineHeight: 1,
375
+ lineHeight: 0,
377
376
  pointerEvents: 'none',
378
- textShadow: `0 0 ${scale}px rgba(0,0,0,0.8)`,
377
+ color: '#ffd700',
378
+ filter: `drop-shadow(0 0 ${Math.max(1, scale)}px rgba(0,0,0,0.85))`,
379
379
  }}
380
380
  >
381
-
381
+ <StarIcon size={Math.max(1, Math.round(6 * scale))} />
382
382
  </span>
383
383
  )}
384
384
  </div>
@@ -0,0 +1,31 @@
1
+ import React from 'react'
2
+
3
+ /** Pixel-art star (18×18 viewBox), same appearance on all platforms */
4
+ const STAR_PATH =
5
+ 'M15.4375 15.1238L15.625 15.3113V16.8113L15.4375 16.9988H14.6875L14.5 16.8113V16.4363L14.3125 16.2488H12.8125L12.625 16.0613V15.3113L12.4375 15.1238H10.9375L10.75 14.9363V14.1863L10.5625 13.9988H7.9375L7.75 14.1863V14.9363L7.5625 15.1238H6.0625L5.875 15.3113V16.0613L5.6875 16.2488H4.1875L4 16.4363V16.8113L3.8125 16.9988H3.0625L2.875 16.8113V15.3113L3.0625 15.1238H3.4375L3.625 14.9363V13.8113L3.8125 13.6238H4.1875L4.375 13.4363V11.9363L4.5625 11.7488H4.9375L5.125 11.5613V10.0613L4.9375 9.87384H4.1875L4 9.68634V8.93634L3.8125 8.74884H2.6875L2.5 8.56134V7.81134L2.3125 7.62384H1.1875L1 7.43634V6.68634L1.1875 6.49884H6.63738L6.82488 6.31134V5.61543V4.60586L7.01238 4.41836H7.5625L7.75 4.23086V2.6875L7.9375 2.5H8.43022L8.61772 2.3125V1.1875L8.80522 1H9.66735L9.85485 1.1875V2.3125L10.0424 2.5H10.5625L10.75 2.6875V4.23086L10.9375 4.41836H11.4375L11.625 4.60586V6.31134L11.8125 6.49884H17.3125L17.5 6.68634V7.43634L17.3125 7.62384H16.1875L16 7.81134V8.56134L15.8125 8.74884H14.6875L14.5 8.93634V9.68634L14.3125 9.87384H13.5625L13.375 10.0613V11.5613L13.5625 11.7488H13.9375L14.125 11.9363V13.4363L14.3125 13.6238H14.6875L14.875 13.8113V14.9363L15.0625 15.1238H15.4375Z'
6
+
7
+ export interface StarIconProps {
8
+ size: number
9
+ className?: string
10
+ style?: React.CSSProperties
11
+ title?: string
12
+ }
13
+
14
+ export function StarIcon({ size, className, style, title }: StarIconProps) {
15
+ return (
16
+ <svg
17
+ width={size}
18
+ height={size}
19
+ viewBox="0 0 18 18"
20
+ fill="none"
21
+ xmlns="http://www.w3.org/2000/svg"
22
+ className={className}
23
+ style={{ display: 'block', flexShrink: 0, ...style }}
24
+ aria-hidden={title ? undefined : true}
25
+ role={title ? 'img' : undefined}
26
+ >
27
+ {title ? <title>{title}</title> : null}
28
+ <path d={STAR_PATH} fill="currentColor" />
29
+ </svg>
30
+ )
31
+ }
@@ -134,14 +134,14 @@ export function Notes({
134
134
  ...style,
135
135
  }}
136
136
  >
137
- <div style={{
137
+ {/* <div style={{
138
138
  padding: `${3 * scale}px ${4 * scale}px`,
139
139
  color: '#404040',
140
140
  fontWeight: 'bold',
141
141
  flexShrink: 0,
142
142
  }}>
143
143
  Current Tasks
144
- </div>
144
+ </div> */}
145
145
 
146
146
  {/* Task list */}
147
147
  <div style={{ overflowY: 'auto', padding: `${2 * scale}px`, minHeight: 0 }}>
@@ -162,11 +162,15 @@ export function Notes({
162
162
  marginBottom: 2 * scale,
163
163
  padding: `${2 * scale}px`,
164
164
  }}>
165
- <span style={{ flex: 1, color: '#222', wordBreak: 'break-word', lineHeight: 1.3 }}>
165
+ <span style={{ flex: 1, color: '#d6d6d6', wordBreak: 'break-word', lineHeight: 1.3 }}>
166
166
  {note.text}
167
167
  </span>
168
168
  <button
169
- onClick={() => removeNote(note.id)}
169
+ type="button"
170
+ onClick={(e) => {
171
+ e.stopPropagation()
172
+ removeNote(note.id)
173
+ }}
170
174
  style={{
171
175
  background: '#8b0000',
172
176
  color: '#fff',
@@ -201,6 +205,7 @@ export function Notes({
201
205
  value={draft}
202
206
  rows={2}
203
207
  autoFocus
208
+ onClick={(e) => e.stopPropagation()}
204
209
  onChange={(e) => {
205
210
  setDraft(e.target.value.replace(/[\r\n]/g, ''))
206
211
  }}
@@ -229,7 +234,12 @@ export function Notes({
229
234
  <div style={{ display: 'flex', flexDirection: 'column', gap: scale, flexShrink: 0 }}>
230
235
  {showInput && (
231
236
  <button
232
- onClick={() => { setShowInput(false); setDraft('') }}
237
+ type="button"
238
+ onClick={(e) => {
239
+ e.stopPropagation()
240
+ setShowInput(false)
241
+ setDraft('')
242
+ }}
233
243
  title="Hide"
234
244
  style={{
235
245
  background: '#5a3a3a',
@@ -246,7 +256,11 @@ export function Notes({
246
256
  </button>
247
257
  )}
248
258
  <button
249
- onClick={handleAddButtonClick}
259
+ type="button"
260
+ onClick={(e) => {
261
+ e.stopPropagation()
262
+ handleAddButtonClick()
263
+ }}
250
264
  disabled={showInput && !draft.trim()}
251
265
  style={{
252
266
  background: '#404040',
@@ -272,6 +272,12 @@ export function Slot({
272
272
 
273
273
  if (pKeyActive) setPKeyActive(false)
274
274
 
275
+ // JEI / recipe / custom slots: same handler as desktop onMouseUp (not focus/swap).
276
+ if (onClickOverride) {
277
+ onClickOverride('left', 'normal')
278
+ return
279
+ }
280
+
275
281
  if (heldItem) {
276
282
  // When holding an item, place it (standard behavior, no focus needed)
277
283
  sendAction({ type: 'click', slotIndex: index, button: 'left', mode: 'normal' })
@@ -291,7 +297,7 @@ export function Slot({
291
297
  setFocusedSlot(null)
292
298
  }
293
299
  },
294
- [isMobile, disabled, heldItem, sendAction, index, pKeyActive, setPKeyActive, focusedSlot, setFocusedSlot],
300
+ [isMobile, disabled, heldItem, sendAction, index, pKeyActive, setPKeyActive, focusedSlot, setFocusedSlot, onClickOverride],
295
301
  )
296
302
 
297
303
  const handleMobilePickAll = useCallback(() => {
@@ -102,6 +102,8 @@ export function InventoryProvider({ connector, children, noDragSpread = false, n
102
102
  // Set to true when endDrag fires; cleared on the next mouseDown in Slot.
103
103
  // Prevents spurious mouseUp events from sending unwanted clicks after a drag.
104
104
  const dragEndedRef = useRef(false)
105
+ /** Latest full context value; updated every render for global debug (`globalThis.__mcInv.state`). */
106
+ const valueRef = useRef<InventoryContextValue | null>(null)
105
107
 
106
108
  useEffect(() => {
107
109
  if (!connector) return
@@ -284,19 +286,6 @@ export function InventoryProvider({ connector, children, noDragSpread = false, n
284
286
  [windowState],
285
287
  )
286
288
 
287
- // Expose state to window for easier debugging from browser DevTools.
288
- // Access via: window.__mcInv
289
- useEffect(() => {
290
- ;(window as unknown as Record<string, unknown>).__mcInv = {
291
- get windowState() { return windowStateRef.current },
292
- get heldItem() { return heldItemRef.current },
293
- get dragSlots() { return dragSlots },
294
- get isDragging() { return isDragging },
295
- get focusedSlot() { return focusedSlot },
296
- get pKeyActive() { return pKeyActive },
297
- }
298
- })
299
-
300
289
  const value: InventoryContextValue = {
301
290
  windowState,
302
291
  playerState,
@@ -331,5 +320,20 @@ export function InventoryProvider({ connector, children, noDragSpread = false, n
331
320
  dragEndedRef,
332
321
  }
333
322
 
323
+ valueRef.current = value
324
+
325
+ // Full inventory UI state on global for DevTools: globalThis.__mcInv.state (e.g. windowState, playerState, heldItem, drag state).
326
+ useEffect(() => {
327
+ const g = globalThis as any
328
+ g.__mcInv = {
329
+ get state() {
330
+ return valueRef.current
331
+ },
332
+ }
333
+ return () => {
334
+ delete g.__mcInv
335
+ }
336
+ }, [])
337
+
334
338
  return <InventoryContext.Provider value={value}>{children}</InventoryContext.Provider>
335
339
  }
@@ -0,0 +1,55 @@
1
+ import { useEffect, useRef } from 'react'
2
+
3
+ function isTypingTarget(el: EventTarget | null): boolean {
4
+ if (!el || !(el instanceof HTMLElement)) return false
5
+ const tag = el.tagName
6
+ if (tag === 'INPUT' || tag === 'TEXTAREA' || tag === 'SELECT') return true
7
+ if (el.isContentEditable) return true
8
+ return false
9
+ }
10
+
11
+ /**
12
+ * While W is held, a primary-button pointerdown anywhere (capture phase) calls onClose.
13
+ * Ignores events when the target is an input/textarea/select or contenteditable.
14
+ */
15
+ export function useCloseOnWClick(onClose?: () => void) {
16
+ const onCloseRef = useRef(onClose)
17
+ onCloseRef.current = onClose
18
+ const wHeldRef = useRef(false)
19
+
20
+ useEffect(() => {
21
+ if (!onClose) return
22
+
23
+ const onKeyDown = (e: KeyboardEvent) => {
24
+ if (e.code === 'KeyW' && !e.repeat) wHeldRef.current = true
25
+ }
26
+ const onKeyUp = (e: KeyboardEvent) => {
27
+ if (e.code === 'KeyW') wHeldRef.current = false
28
+ }
29
+ const resetW = () => {
30
+ wHeldRef.current = false
31
+ }
32
+
33
+ const onPointerDownCapture = (e: PointerEvent) => {
34
+ if (e.button !== 0 || !wHeldRef.current) return
35
+ if (isTypingTarget(e.target)) return
36
+ const cb = onCloseRef.current
37
+ if (!cb) return
38
+ cb()
39
+ e.preventDefault()
40
+ e.stopPropagation()
41
+ }
42
+
43
+ window.addEventListener('keydown', onKeyDown)
44
+ window.addEventListener('keyup', onKeyUp)
45
+ window.addEventListener('blur', resetW)
46
+ document.addEventListener('pointerdown', onPointerDownCapture, true)
47
+
48
+ return () => {
49
+ window.removeEventListener('keydown', onKeyDown)
50
+ window.removeEventListener('keyup', onKeyUp)
51
+ window.removeEventListener('blur', resetW)
52
+ document.removeEventListener('pointerdown', onPointerDownCapture, true)
53
+ }
54
+ }, [onClose])
55
+ }
@@ -452,6 +452,9 @@ export const inventoryDefinitions = makeInventoryDefinitions({
452
452
  topEnchantId: { dataSlot: 4, description: 'Top enchantment ID' },
453
453
  middleEnchantId: { dataSlot: 5, description: 'Middle enchantment ID' },
454
454
  bottomEnchantId: { dataSlot: 6, description: 'Bottom enchantment ID' },
455
+ topLevelClue: { dataSlot: 7, description: 'Top enchantment level clue (e.g. III)' },
456
+ middleLevelClue: { dataSlot: 8, description: 'Middle enchantment level clue (e.g. II)' },
457
+ bottomLevelClue: { dataSlot: 9, description: 'Bottom enchantment level clue (e.g. I)' },
455
458
  },
456
459
  slots: [
457
460
  { x: 15, y: 47, group: 'enchant' },
@@ -645,7 +648,7 @@ export const inventoryDefinitions = makeInventoryDefinitions({
645
648
  backgroundHeight: 166,
646
649
  slots: [
647
650
  { x: 15, y: 15, group: 'input', label: 'Map' },
648
- { x: 15, y: 39, group: 'input', label: 'Paper/Map' },
651
+ { x: 15, y: 52, group: 'input', label: 'Paper/Map' },
649
652
  { x: 145, y: 39, group: 'result', resultSlot: true },
650
653
  ...playerInv(84),
651
654
  ],