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,56 @@
|
|
|
1
|
+
.tooltip {
|
|
2
|
+
background: #100010;
|
|
3
|
+
border: 1px solid #100010;
|
|
4
|
+
border-radius: 5px;
|
|
5
|
+
display: flex;
|
|
6
|
+
flex-direction: column;
|
|
7
|
+
font-family: 'Minecraftia', 'Minecraft', monospace;
|
|
8
|
+
image-rendering: pixelated;
|
|
9
|
+
line-height: 1.2;
|
|
10
|
+
/* MC tooltip: 2-layer purple gradient inner glow */
|
|
11
|
+
box-shadow:
|
|
12
|
+
inset 0 1px 0 0 #5000a0,
|
|
13
|
+
inset 1px 0 0 0 #5000a0,
|
|
14
|
+
inset -1px 0 0 0 #5000a0,
|
|
15
|
+
inset 0 -1px 0 0 #28007f,
|
|
16
|
+
0 0 0 1px #100010;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
.name {
|
|
20
|
+
font-weight: bold;
|
|
21
|
+
white-space: nowrap;
|
|
22
|
+
margin-bottom: 2px;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
.section {
|
|
26
|
+
display: flex;
|
|
27
|
+
flex-direction: column;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
.enchantment {
|
|
31
|
+
color: #8080ff;
|
|
32
|
+
white-space: nowrap;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
.lore {
|
|
36
|
+
display: flex;
|
|
37
|
+
flex-direction: column;
|
|
38
|
+
color: #6b29a4;
|
|
39
|
+
font-style: italic;
|
|
40
|
+
border-top: 1px solid #2d0069;
|
|
41
|
+
margin-top: 3px;
|
|
42
|
+
padding-top: 3px;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
.durability {
|
|
46
|
+
color: #dddddd;
|
|
47
|
+
border-top: 1px solid #2d0069;
|
|
48
|
+
margin-top: 3px;
|
|
49
|
+
padding-top: 3px;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
.typeId {
|
|
53
|
+
color: #444455;
|
|
54
|
+
font-size: 0.8em;
|
|
55
|
+
margin-top: 2px;
|
|
56
|
+
}
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
import React, { useRef, useState, useEffect, useLayoutEffect } from 'react'
|
|
2
|
+
import { createPortal } from 'react-dom'
|
|
3
|
+
import type { ItemStack } from '../../types'
|
|
4
|
+
import { useScale } from '../../context/ScaleContext'
|
|
5
|
+
import { MessageFormattedString } from '../Text/MessageFormattedString'
|
|
6
|
+
import styles from './Tooltip.module.css'
|
|
7
|
+
|
|
8
|
+
interface TooltipProps {
|
|
9
|
+
item: ItemStack
|
|
10
|
+
mouseX: number
|
|
11
|
+
mouseY: number
|
|
12
|
+
visible: boolean
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function getRarityColor(item: ItemStack): string {
|
|
16
|
+
if (item.enchantments && item.enchantments.length > 0) return '#8080ff'
|
|
17
|
+
return '#ffffff'
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function formatEnchantment(e: { name: string; level: number }): string {
|
|
21
|
+
const roman = ['', 'I', 'II', 'III', 'IV', 'V', 'VI', 'VII', 'VIII', 'IX', 'X']
|
|
22
|
+
return e.level <= 10 ? `${e.name} ${roman[e.level]}` : `${e.name} ${e.level}`
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export function Tooltip({ item, mouseX, mouseY, visible }: TooltipProps) {
|
|
26
|
+
const { scale } = useScale()
|
|
27
|
+
const ref = useRef<HTMLDivElement>(null)
|
|
28
|
+
const [pos, setPos] = useState({ x: -9999, y: -9999 })
|
|
29
|
+
const [ready, setReady] = useState(false)
|
|
30
|
+
|
|
31
|
+
// Recompute position every time mouse moves or item changes
|
|
32
|
+
useLayoutEffect(() => {
|
|
33
|
+
if (!visible || !ref.current) return
|
|
34
|
+
const el = ref.current
|
|
35
|
+
const tw = el.offsetWidth
|
|
36
|
+
const th = el.offsetHeight
|
|
37
|
+
const vw = window.innerWidth
|
|
38
|
+
const vh = window.innerHeight
|
|
39
|
+
const gapX = 12
|
|
40
|
+
const gapY = 2
|
|
41
|
+
|
|
42
|
+
let x = mouseX + gapX
|
|
43
|
+
let y = mouseY - th - gapY // above cursor by default
|
|
44
|
+
|
|
45
|
+
if (x + tw > vw - 4) x = mouseX - tw - gapX
|
|
46
|
+
if (y < 4) y = mouseY + gapY // flip below if no space above
|
|
47
|
+
if (y + th > vh - 4) y = vh - th - 4
|
|
48
|
+
|
|
49
|
+
setPos({ x, y })
|
|
50
|
+
setReady(true)
|
|
51
|
+
}, [mouseX, mouseY, visible, item, scale])
|
|
52
|
+
|
|
53
|
+
// Reset ready when tooltip becomes hidden
|
|
54
|
+
useEffect(() => {
|
|
55
|
+
if (!visible) setReady(false)
|
|
56
|
+
}, [visible])
|
|
57
|
+
|
|
58
|
+
if (!visible) return null
|
|
59
|
+
|
|
60
|
+
const nameColor = getRarityColor(item)
|
|
61
|
+
const hasDurability =
|
|
62
|
+
item.durability !== undefined &&
|
|
63
|
+
item.maxDurability !== undefined &&
|
|
64
|
+
item.durability < item.maxDurability
|
|
65
|
+
|
|
66
|
+
const fs = Math.round(8 * scale)
|
|
67
|
+
const pad = Math.round(4 * scale)
|
|
68
|
+
const gap2 = Math.round(1 * scale)
|
|
69
|
+
|
|
70
|
+
const tooltip = (
|
|
71
|
+
<div
|
|
72
|
+
ref={ref}
|
|
73
|
+
className={['mc-inv-tooltip', styles.tooltip].join(' ')}
|
|
74
|
+
style={{
|
|
75
|
+
position: 'fixed',
|
|
76
|
+
left: pos.x,
|
|
77
|
+
top: pos.y,
|
|
78
|
+
zIndex: 10000,
|
|
79
|
+
fontSize: fs,
|
|
80
|
+
padding: pad,
|
|
81
|
+
gap: gap2,
|
|
82
|
+
minWidth: Math.round(80 * scale),
|
|
83
|
+
maxWidth: Math.round(220 * scale),
|
|
84
|
+
visibility: ready ? 'visible' : 'hidden',
|
|
85
|
+
pointerEvents: 'none',
|
|
86
|
+
}}
|
|
87
|
+
>
|
|
88
|
+
<div className={styles.name} style={{ color: nameColor }}>
|
|
89
|
+
<MessageFormattedString
|
|
90
|
+
text={item.displayName ?? (item.name ? item.name.replace(/_/g, ' ') : `Item #${item.type}`)}
|
|
91
|
+
fallbackColor={nameColor}
|
|
92
|
+
/>
|
|
93
|
+
</div>
|
|
94
|
+
|
|
95
|
+
{item.enchantments && item.enchantments.length > 0 && (
|
|
96
|
+
<div className={styles.section}>
|
|
97
|
+
{item.enchantments.map((e, i) => (
|
|
98
|
+
<div key={i} className={styles.enchantment}>
|
|
99
|
+
{formatEnchantment(e)}
|
|
100
|
+
</div>
|
|
101
|
+
))}
|
|
102
|
+
</div>
|
|
103
|
+
)}
|
|
104
|
+
|
|
105
|
+
{item.lore && item.lore.length > 0 && (
|
|
106
|
+
<div className={styles.lore}>
|
|
107
|
+
{item.lore.map((line, i) => (
|
|
108
|
+
<div key={i}>
|
|
109
|
+
<MessageFormattedString text={line} />
|
|
110
|
+
</div>
|
|
111
|
+
))}
|
|
112
|
+
</div>
|
|
113
|
+
)}
|
|
114
|
+
|
|
115
|
+
{hasDurability && (
|
|
116
|
+
<div className={styles.durability}>
|
|
117
|
+
Durability: {item.durability} / {item.maxDurability}
|
|
118
|
+
</div>
|
|
119
|
+
)}
|
|
120
|
+
|
|
121
|
+
<div className={styles.typeId}>
|
|
122
|
+
{item.name ?? `#${item.type}`}
|
|
123
|
+
{item.metadata !== undefined && item.metadata > 0 ? `:${item.metadata}` : ''}
|
|
124
|
+
</div>
|
|
125
|
+
</div>
|
|
126
|
+
)
|
|
127
|
+
|
|
128
|
+
// Portal to document.body so position:fixed works even inside transformed ancestors
|
|
129
|
+
return createPortal(tooltip, document.body)
|
|
130
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { Tooltip } from './Tooltip'
|
|
@@ -0,0 +1,213 @@
|
|
|
1
|
+
import type { InventoryAction, InventoryWindowState, PlayerState, SlotState, ItemStack } from '../types'
|
|
2
|
+
import type { InventoryConnector, ConnectorListener, ConnectorEvent } from './types'
|
|
3
|
+
|
|
4
|
+
export interface ActionLogEntry {
|
|
5
|
+
id: number
|
|
6
|
+
timestamp: number
|
|
7
|
+
action: InventoryAction
|
|
8
|
+
description: string
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export interface DemoConnectorOptions {
|
|
12
|
+
windowType: string
|
|
13
|
+
windowTitle?: string
|
|
14
|
+
slots?: SlotState[]
|
|
15
|
+
playerInventory?: SlotState[]
|
|
16
|
+
onAction?: (entry: ActionLogEntry) => void
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
let actionCounter = 0
|
|
20
|
+
|
|
21
|
+
function describeAction(action: InventoryAction): string {
|
|
22
|
+
switch (action.type) {
|
|
23
|
+
case 'click':
|
|
24
|
+
return `${action.mode === 'shift' ? 'Shift-' : ''}${action.button === 'right' ? 'Right' : 'Left'} click slot ${action.slotIndex}${action.mode === 'number' ? ` (hotbar ${action.numberKey})` : ''}`
|
|
25
|
+
case 'drop':
|
|
26
|
+
return `Drop ${action.all ? 'all' : 'one'} from slot ${action.slotIndex}`
|
|
27
|
+
case 'drag':
|
|
28
|
+
return `Drag ${action.button} across slots: ${action.slots.join(', ')}`
|
|
29
|
+
case 'close':
|
|
30
|
+
return 'Close window'
|
|
31
|
+
case 'trade':
|
|
32
|
+
return `Trade at index ${action.tradeIndex}`
|
|
33
|
+
case 'rename':
|
|
34
|
+
return `Rename to "${action.text}"`
|
|
35
|
+
case 'enchant':
|
|
36
|
+
return `Select enchantment ${action.enchantIndex}`
|
|
37
|
+
case 'beacon':
|
|
38
|
+
return `Set beacon effects: ${action.primaryEffect} / ${action.secondaryEffect}`
|
|
39
|
+
case 'hotbar-swap':
|
|
40
|
+
return `Swap slot ${action.slotIndex} with hotbar ${action.hotbarSlot}`
|
|
41
|
+
default:
|
|
42
|
+
return JSON.stringify(action)
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export function createDemoConnector(options: DemoConnectorOptions): InventoryConnector & {
|
|
47
|
+
actionLog: ActionLogEntry[]
|
|
48
|
+
updateSlots(slots: SlotState[]): void
|
|
49
|
+
setHeldItem(item: ItemStack | null): void
|
|
50
|
+
openWindow(type: string, title: string, slots: SlotState[]): void
|
|
51
|
+
closeWindowExternal(): void
|
|
52
|
+
} {
|
|
53
|
+
const listeners = new Set<ConnectorListener>()
|
|
54
|
+
const actionLog: ActionLogEntry[] = []
|
|
55
|
+
|
|
56
|
+
let windowState: InventoryWindowState | null = {
|
|
57
|
+
windowId: 1,
|
|
58
|
+
type: options.windowType,
|
|
59
|
+
title: options.windowTitle ?? options.windowType,
|
|
60
|
+
slots: options.slots ?? [],
|
|
61
|
+
heldItem: null,
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
let playerState: PlayerState = {
|
|
65
|
+
activeHotbarSlot: 0,
|
|
66
|
+
inventory: options.playerInventory ?? [],
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function emit(event: ConnectorEvent) {
|
|
70
|
+
listeners.forEach((l) => l(event))
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
return {
|
|
74
|
+
actionLog,
|
|
75
|
+
|
|
76
|
+
getWindowState: () => windowState,
|
|
77
|
+
getPlayerState: () => playerState,
|
|
78
|
+
|
|
79
|
+
sendAction: (action: InventoryAction) => {
|
|
80
|
+
const entry: ActionLogEntry = {
|
|
81
|
+
id: ++actionCounter,
|
|
82
|
+
timestamp: Date.now(),
|
|
83
|
+
action,
|
|
84
|
+
description: describeAction(action),
|
|
85
|
+
}
|
|
86
|
+
actionLog.unshift(entry)
|
|
87
|
+
if (actionLog.length > 100) actionLog.splice(100)
|
|
88
|
+
options.onAction?.(entry)
|
|
89
|
+
|
|
90
|
+
// Demo: simulate simple click behavior
|
|
91
|
+
if (action.type === 'click' && windowState) {
|
|
92
|
+
const slots = [...windowState.slots]
|
|
93
|
+
const slotState = slots.find((s) => s.index === action.slotIndex)
|
|
94
|
+
const held = windowState.heldItem
|
|
95
|
+
|
|
96
|
+
if (action.button === 'left' && action.mode === 'normal') {
|
|
97
|
+
if (held && slotState) {
|
|
98
|
+
const idx = slots.indexOf(slotState)
|
|
99
|
+
const existing = slotState.item
|
|
100
|
+
if (!existing) {
|
|
101
|
+
slots[idx] = { ...slotState, item: held }
|
|
102
|
+
windowState = { ...windowState, slots, heldItem: null }
|
|
103
|
+
} else if (
|
|
104
|
+
existing.type === held.type &&
|
|
105
|
+
(existing.name ?? '') === (held.name ?? '') &&
|
|
106
|
+
(existing.metadata ?? 0) === (held.metadata ?? 0)
|
|
107
|
+
) {
|
|
108
|
+
// Same item type — combine stacks
|
|
109
|
+
const maxStack = 64
|
|
110
|
+
const space = maxStack - existing.count
|
|
111
|
+
if (space > 0) {
|
|
112
|
+
const take = Math.min(held.count, space)
|
|
113
|
+
slots[idx] = { ...slotState, item: { ...existing, count: existing.count + take } }
|
|
114
|
+
const remain = held.count - take
|
|
115
|
+
windowState = { ...windowState, slots, heldItem: remain > 0 ? { ...held, count: remain } : null }
|
|
116
|
+
} else {
|
|
117
|
+
// Stack full — swap
|
|
118
|
+
slots[idx] = { ...slotState, item: held }
|
|
119
|
+
windowState = { ...windowState, slots, heldItem: existing }
|
|
120
|
+
}
|
|
121
|
+
} else {
|
|
122
|
+
// Different item types — swap
|
|
123
|
+
slots[idx] = { ...slotState, item: held }
|
|
124
|
+
windowState = { ...windowState, slots, heldItem: existing }
|
|
125
|
+
}
|
|
126
|
+
} else if (!held && slotState?.item) {
|
|
127
|
+
const idx = slots.indexOf(slotState)
|
|
128
|
+
slots[idx] = { ...slotState, item: null }
|
|
129
|
+
windowState = { ...windowState, slots, heldItem: slotState.item }
|
|
130
|
+
}
|
|
131
|
+
} else if (action.button === 'right' && action.mode === 'normal') {
|
|
132
|
+
if (!held && slotState?.item) {
|
|
133
|
+
const item = slotState.item
|
|
134
|
+
const half = Math.ceil(item.count / 2)
|
|
135
|
+
const remain = item.count - half
|
|
136
|
+
const idx = slots.indexOf(slotState)
|
|
137
|
+
slots[idx] = { ...slotState, item: remain > 0 ? { ...item, count: remain } : null }
|
|
138
|
+
windowState = { ...windowState, slots, heldItem: { ...item, count: half } }
|
|
139
|
+
} else if (held && slotState) {
|
|
140
|
+
const idx = slots.indexOf(slotState)
|
|
141
|
+
const existing = slotState.item
|
|
142
|
+
if (!existing || existing.type === held.type) {
|
|
143
|
+
slots[idx] = {
|
|
144
|
+
...slotState,
|
|
145
|
+
item: { ...held, count: (existing?.count ?? 0) + 1 },
|
|
146
|
+
}
|
|
147
|
+
const newCount = held.count - 1
|
|
148
|
+
windowState = {
|
|
149
|
+
...windowState,
|
|
150
|
+
slots,
|
|
151
|
+
heldItem: newCount > 0 ? { ...held, count: newCount } : null,
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
} else if (action.mode === 'shift' && slotState?.item) {
|
|
156
|
+
// Simulate shift-click: just log it
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
emit({ type: 'windowUpdate', state: windowState })
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
if (action.type === 'drop' && windowState) {
|
|
163
|
+
const slots = [...windowState.slots]
|
|
164
|
+
const slotState = slots.find((s) => s.index === action.slotIndex)
|
|
165
|
+
if (slotState?.item) {
|
|
166
|
+
const idx = slots.indexOf(slotState)
|
|
167
|
+
if (action.all) {
|
|
168
|
+
slots[idx] = { ...slotState, item: null }
|
|
169
|
+
} else {
|
|
170
|
+
const count = slotState.item.count - 1
|
|
171
|
+
slots[idx] = { ...slotState, item: count > 0 ? { ...slotState.item, count } : null }
|
|
172
|
+
}
|
|
173
|
+
windowState = { ...windowState, slots }
|
|
174
|
+
emit({ type: 'windowUpdate', state: windowState })
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
},
|
|
178
|
+
|
|
179
|
+
closeWindow: () => {
|
|
180
|
+
windowState = null
|
|
181
|
+
emit({ type: 'windowClose' })
|
|
182
|
+
},
|
|
183
|
+
|
|
184
|
+
subscribe: (listener: ConnectorListener) => {
|
|
185
|
+
listeners.add(listener)
|
|
186
|
+
return () => listeners.delete(listener)
|
|
187
|
+
},
|
|
188
|
+
|
|
189
|
+
updateSlots: (slots: SlotState[]) => {
|
|
190
|
+
if (windowState) {
|
|
191
|
+
windowState = { ...windowState, slots }
|
|
192
|
+
emit({ type: 'windowUpdate', state: windowState })
|
|
193
|
+
}
|
|
194
|
+
},
|
|
195
|
+
|
|
196
|
+
setHeldItem: (item: ItemStack | null) => {
|
|
197
|
+
if (windowState) {
|
|
198
|
+
windowState = { ...windowState, heldItem: item }
|
|
199
|
+
emit({ type: 'heldItemChange', item })
|
|
200
|
+
}
|
|
201
|
+
},
|
|
202
|
+
|
|
203
|
+
openWindow: (type: string, title: string, slots: SlotState[]) => {
|
|
204
|
+
windowState = { windowId: ++actionCounter, type, title, slots, heldItem: null }
|
|
205
|
+
emit({ type: 'windowOpen', state: windowState })
|
|
206
|
+
},
|
|
207
|
+
|
|
208
|
+
closeWindowExternal: () => {
|
|
209
|
+
windowState = null
|
|
210
|
+
emit({ type: 'windowClose' })
|
|
211
|
+
},
|
|
212
|
+
}
|
|
213
|
+
}
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
export type { InventoryConnector, ConnectorEvent, ConnectorListener, MineflayerBot } from './types'
|
|
2
|
+
export { createMineflayerConnector } from './mineflayer'
|
|
3
|
+
export { createDemoConnector } from './demo'
|
|
4
|
+
export type { DemoConnectorOptions, ActionLogEntry } from './demo'
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
import type { InventoryAction, InventoryWindowState, PlayerState, SlotState, ItemStack } from '../types'
|
|
2
|
+
import type { InventoryConnector, ConnectorListener, ConnectorEvent, MineflayerBot } from './types'
|
|
3
|
+
|
|
4
|
+
function botSlotToItemStack(slot: MineflayerBot['heldItem']): ItemStack | null {
|
|
5
|
+
if (!slot || slot.type === -1 || slot.type === 0) return null
|
|
6
|
+
return {
|
|
7
|
+
type: slot.type,
|
|
8
|
+
count: slot.count,
|
|
9
|
+
metadata: slot.metadata,
|
|
10
|
+
nbt: slot.nbt as Record<string, unknown> | undefined,
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
function botSlotsToSlotStates(slots: MineflayerBot['inventory']['slots']): SlotState[] {
|
|
15
|
+
return slots.map((slot, index) => ({
|
|
16
|
+
index,
|
|
17
|
+
item: botSlotToItemStack(slot),
|
|
18
|
+
}))
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function modeFromAction(action: InventoryAction): [number, number] {
|
|
22
|
+
if (action.type !== 'click') return [0, 0]
|
|
23
|
+
if (action.mode === 'shift') return [action.button === 'left' ? 0 : 1, 1]
|
|
24
|
+
if (action.mode === 'number' && action.numberKey !== undefined) return [action.numberKey, 2]
|
|
25
|
+
if (action.mode === 'middle') return [2, 3]
|
|
26
|
+
if (action.mode === 'drop') return [action.button === 'left' ? 0 : 1, 4]
|
|
27
|
+
if (action.mode === 'drag') return [action.button === 'left' ? 0 : 4, 5]
|
|
28
|
+
if (action.mode === 'double') return [0, 6]
|
|
29
|
+
return [action.button === 'left' ? 0 : 1, 0]
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export function createMineflayerConnector(bot: MineflayerBot): InventoryConnector {
|
|
33
|
+
const listeners = new Set<ConnectorListener>()
|
|
34
|
+
|
|
35
|
+
function emit(event: ConnectorEvent) {
|
|
36
|
+
listeners.forEach((l) => l(event))
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function buildWindowState(): InventoryWindowState | null {
|
|
40
|
+
const win = bot.currentWindow
|
|
41
|
+
if (!win) return null
|
|
42
|
+
return {
|
|
43
|
+
windowId: win.id,
|
|
44
|
+
type: win.type ?? 'unknown',
|
|
45
|
+
title: win.title,
|
|
46
|
+
slots: botSlotsToSlotStates(win.slots),
|
|
47
|
+
heldItem: botSlotToItemStack(bot.heldItem),
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function buildPlayerState(): PlayerState {
|
|
52
|
+
const inv = bot.inventory.slots
|
|
53
|
+
return {
|
|
54
|
+
activeHotbarSlot: bot.quickBarSlot,
|
|
55
|
+
inventory: botSlotsToSlotStates(inv),
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
const onWindowOpen = () => {
|
|
60
|
+
const state = buildWindowState()
|
|
61
|
+
if (state) emit({ type: 'windowOpen', state })
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
const onWindowClose = () => {
|
|
65
|
+
emit({ type: 'windowClose' })
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
const onSetSlot = () => {
|
|
69
|
+
const state = buildWindowState()
|
|
70
|
+
if (state) emit({ type: 'windowUpdate', state })
|
|
71
|
+
else {
|
|
72
|
+
emit({ type: 'playerUpdate', state: buildPlayerState() })
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
bot.on('windowOpen', onWindowOpen)
|
|
77
|
+
bot.on('windowClose', onWindowClose)
|
|
78
|
+
bot.on('setSlot', onSetSlot)
|
|
79
|
+
|
|
80
|
+
return {
|
|
81
|
+
getWindowState: buildWindowState,
|
|
82
|
+
getPlayerState: buildPlayerState,
|
|
83
|
+
|
|
84
|
+
sendAction: async (action: InventoryAction) => {
|
|
85
|
+
if (action.type === 'click') {
|
|
86
|
+
const [mouseButton, mode] = modeFromAction(action)
|
|
87
|
+
await bot.clickWindow(action.slotIndex, mouseButton, mode)
|
|
88
|
+
} else if (action.type === 'drop') {
|
|
89
|
+
await bot.clickWindow(action.slotIndex, action.all ? 1 : 0, 4)
|
|
90
|
+
} else if (action.type === 'close') {
|
|
91
|
+
const win = bot.currentWindow
|
|
92
|
+
if (win) bot.closeWindow(win)
|
|
93
|
+
} else if (action.type === 'hotbar-swap') {
|
|
94
|
+
await bot.clickWindow(action.slotIndex, action.hotbarSlot, 2)
|
|
95
|
+
}
|
|
96
|
+
},
|
|
97
|
+
|
|
98
|
+
closeWindow: () => {
|
|
99
|
+
const win = bot.currentWindow
|
|
100
|
+
if (win) bot.closeWindow(win)
|
|
101
|
+
},
|
|
102
|
+
|
|
103
|
+
subscribe: (listener: ConnectorListener) => {
|
|
104
|
+
listeners.add(listener)
|
|
105
|
+
return () => {
|
|
106
|
+
listeners.delete(listener)
|
|
107
|
+
bot.off('windowOpen', onWindowOpen)
|
|
108
|
+
bot.off('windowClose', onWindowClose)
|
|
109
|
+
bot.off('setSlot', onSetSlot)
|
|
110
|
+
}
|
|
111
|
+
},
|
|
112
|
+
}
|
|
113
|
+
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import type { InventoryAction, InventoryWindowState, PlayerState, TradeOffer } from '../types'
|
|
2
|
+
|
|
3
|
+
export interface InventoryConnector {
|
|
4
|
+
getWindowState(): InventoryWindowState | null
|
|
5
|
+
getPlayerState(): PlayerState | null
|
|
6
|
+
sendAction(action: InventoryAction): void | Promise<void>
|
|
7
|
+
closeWindow(): void | Promise<void>
|
|
8
|
+
subscribe(listener: ConnectorListener): () => void
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export type ConnectorEvent =
|
|
12
|
+
| { type: 'windowOpen'; state: InventoryWindowState }
|
|
13
|
+
| { type: 'windowClose' }
|
|
14
|
+
| { type: 'windowUpdate'; state: InventoryWindowState }
|
|
15
|
+
| { type: 'playerUpdate'; state: PlayerState }
|
|
16
|
+
| { type: 'heldItemChange'; item: InventoryWindowState['heldItem'] }
|
|
17
|
+
|
|
18
|
+
export type ConnectorListener = (event: ConnectorEvent) => void
|
|
19
|
+
|
|
20
|
+
export interface MineflayerBot {
|
|
21
|
+
inventory: {
|
|
22
|
+
slots: Array<{ type: number; count: number; metadata?: number; nbt?: unknown } | null>
|
|
23
|
+
}
|
|
24
|
+
currentWindow: {
|
|
25
|
+
id: number
|
|
26
|
+
type: string
|
|
27
|
+
title?: string
|
|
28
|
+
slots: Array<{ type: number; count: number; metadata?: number; nbt?: unknown } | null>
|
|
29
|
+
} | null
|
|
30
|
+
heldItem: { type: number; count: number; metadata?: number; nbt?: unknown } | null
|
|
31
|
+
health: number
|
|
32
|
+
food: number
|
|
33
|
+
foodSaturation: number
|
|
34
|
+
experience: { level: number; progress: number; points: number }
|
|
35
|
+
quickBarSlot: number
|
|
36
|
+
clickWindow(slot: number, mouseButton: number, mode: number): Promise<void>
|
|
37
|
+
closeWindow(window: unknown): void
|
|
38
|
+
on(event: string, listener: (...args: unknown[]) => void): void
|
|
39
|
+
off(event: string, listener: (...args: unknown[]) => void): void
|
|
40
|
+
removeListener(event: string, listener: (...args: unknown[]) => void): void
|
|
41
|
+
}
|