# minecraft-inventory

DEMO: https://minecraft-inventory-npm.vercel.app/

A flexible, scalable React library for rendering Minecraft inventory GUIs. Designed for integration into real Minecraft clients (e.g., mineflayer bots, web clients) without using CSS `transform: scale` — all sizing is driven by CSS custom properties.

## Features

- ALL standard inventory types (chest, furnace, crafting table, enchanting table, brewing stand, anvil, smithing table, horse, **beacon**, and more)
- CSS variable–based scaling — no layout jank, no `transform: scale`
- `<img>`-rendered item textures with automatic `items/` → `blocks/` fallback (via PrismarineJS asset mirror by default)
- Tooltips that follow the cursor, matching the original Minecraft style
- Full keyboard support: number keys (1-9) to swap hotbar, Q to drop, double-click to collect, scroll wheel to pick/place
- Mobile support: tap-to-focus inventory interactions plus long-press slot actions (pick half / custom amount / drop one / drop all)
- Optional JEI (item browser) panel on right
- Bot connector layer — plugs into a mineflayer bot or any custom server connection
- Demo connector with action logging for local development
- Extendable registry — add new inventory types in one place

---

## Quick Start (React)

```tsx
import React from 'react'
import { createRoot } from 'react-dom/client'
import { InventoryGUI, createDemoConnector } from 'minecraft-inventory'

const connector = createDemoConnector({
  windowType: 'chest',
  windowTitle: 'My Chest',
  slots: [
    { index: 0, item: { type: 265, name: 'iron_ingot', count: 32, displayName: 'Iron Ingot' } },
    // ...more slots
  ],
})

function App() {
  return (
    <InventoryGUI
      type="chest"
      connector={connector}
      scale={2}
      showJEI
      jeiItems={[
        { type: 1, name: 'stone', displayName: 'Stone', count: 1 },
        { type: 4, name: 'cobblestone', displayName: 'Cobblestone', count: 1 },
      ]}
    />
  )
}

// Mount into the DOM
createRoot(document.getElementById('root')!).render(<App />)
```

## Quick Start (No React — programmatic)

If you don't use React, use the `mountInventory` helper:

```html
<div id="inventory"></div>
<script type="module">
  import { mountInventory, createDemoConnector } from 'minecraft-inventory'

  const connector = createDemoConnector({
    windowType: 'chest',
    windowTitle: 'My Chest',
    slots: [
      { index: 0, item: { type: 265, name: 'iron_ingot', count: 32, displayName: 'Iron Ingot' } },
    ],
  })

  const inv = mountInventory(document.getElementById('inventory'), {
    type: 'chest',
    connector,
    scale: 2,
  })

  // Update props later
  inv.update({ scale: 3 })

  // Destroy when done
  inv.destroy()
</script>
```

---

## Installation

```bash
npm install minecraft-inventory
# or
pnpm add minecraft-inventory
```

---

## Core API

### `<InventoryGUI>` — All-in-one component

```tsx
<InventoryGUI
  type="chest"                    // Inventory type name (see registry)
  title="My Chest"                // Override window title
  slots={[...]}                   // Optional: provide slots directly
  properties={{ litTime: 100 }}   // Optional: progress bar data
  connector={connector}           // Optional: bot connector
  scale={2}                       // Scale multiplier (default: 2)
  showJEI={false}                 // Show item browser panel
  jeiItems={[...]}                // Items to show in JEI
  jeiPosition="right"             // 'left' | 'right'
  textureBaseUrl="/assets/mc"     // Override texture base URL
  enableKeyboardShortcuts={true}
  onClose={() => {}}
/>
```

### `<InventoryOverlay>` — Full-screen overlay with backdrop

Renders the inventory centered on screen with a semi-transparent backdrop. Clicking outside drops the held item or closes the window.

```tsx
import { InventoryOverlay, InventoryProvider, ScaleProvider, TextureProvider } from 'minecraft-inventory'

<TextureProvider>
  <ScaleProvider scale={2}>
    <InventoryProvider connector={connector}>
      <InventoryOverlay
        type="chest"
        showBackdrop            // default: true (50% black)
        backdropColor="rgba(0,0,0,0.5)"
        showJEI
        jeiItems={items}
        jeiPosition="right"
        onClose={() => console.log('closed')}
      >
        {/* Extra children (e.g. notes panel) are rendered inside the centered anchor */}
      </InventoryOverlay>
    </InventoryProvider>
  </ScaleProvider>
</TextureProvider>
```

### Manual composition

For full control, use the provider components and compose manually:

```tsx
import {
  TextureProvider, ScaleProvider, InventoryProvider,
  InventoryWindow, Hotbar, HUD, CursorItem, JEI,
} from 'minecraft-inventory'

function MyInventory({ connector }) {
  return (
    <TextureProvider baseUrl="https://example.com/assets">
      <ScaleProvider scale={2}>
        <InventoryProvider connector={connector}>
          <div style={{ display: 'flex', gap: 8 }}>
            <InventoryWindow type="furnace" />
            <JEI items={allItems} position="right" />
          </div>
          <Hotbar />
          <HUD />
          <CursorItem />
        </InventoryProvider>
      </ScaleProvider>
    </TextureProvider>
  )
}
```

---

## Scaling

Scale is driven entirely by CSS custom properties — no `transform: scale`. Changing the scale prop recalculates all size tokens:

| Variable | Default (scale=2) |
|---|---|
| `--mc-scale` | `2` |
| `--mc-slot-size` | `36px` (18 × scale) |
| `--mc-font-size` | `14px` (7 × scale) |
| `--mc-pixel` | `2px` |

```tsx
// Changing scale re-renders the whole GUI at new dimensions
<InventoryGUI type="chest" scale={3} />
```

---

## Textures

By default, item textures are fetched from the PrismarineJS Minecraft assets mirror on GitHub:

```
https://raw.githubusercontent.com/PrismarineJS/minecraft-assets/master/data/1.21.4/item/{name}.png
```

Provide `name` on each `ItemStack` to use the correct asset:

```ts
{ type: 276, name: 'diamond_sword', count: 1, displayName: 'Diamond Sword' }
```

To use local textures (e.g., from a resource pack server):

```tsx
<InventoryGUI
  type="chest"
  textureBaseUrl="/assets/minecraft"
  // → /assets/minecraft/item/diamond_sword.png
/>
```

For full control, pass a custom texture config overriding any or all URL resolvers:

```tsx
import { TextureProvider } from 'minecraft-inventory'

// Option A — simple base URL (all textures served from your CDN/server)
<TextureProvider baseUrl="https://yourserver.com/mc-assets/1.21.4">
  {/* item textures: /mc-assets/1.21.4/items/{name}.png           */}
  {/* block textures: /mc-assets/1.21.4/blocks/{name}.png         */}
  {/* GUI textures: /mc-assets/1.21.4/textures/{path}.png         */}
  <InventoryWindow type="chest" />
</TextureProvider>

// Option B — replace individual resolvers (config is merged with defaults)
<TextureProvider config={{
  getItemTextureUrl: ({ type, name }) =>
    `https://cdn.example.com/items/${name ?? type}.png`,

  getBlockTextureUrl: ({ type, name }) =>
    `https://cdn.example.com/blocks/${name ?? type}.png`,

  // version param lets you vary GUI textures per container (1.16.4, 1.21.4, etc.)
  getGuiTextureUrl: (path, version = '1.16.4') =>
    `https://cdn.example.com/gui/${version}/${path}.png`,
}}>
  <InventoryWindow type="furnace" />
</TextureProvider>

// Option C — completely static local assets (no CDN)
<TextureProvider config={{
  baseUrl: '/assets',
  getItemTextureUrl: ({ name, type }) => `/assets/items/${name ?? type}.png`,
  getBlockTextureUrl: ({ name, type }) => `/assets/blocks/${name ?? type}.png`,
  getGuiTextureUrl: (path) => `/assets/gui/${path}.png`,
}}>
  <InventoryWindow type="chest" />
</TextureProvider>
```

**Bundled GUI textures (mc-assets)** — Use the optional `mc-assets` package to bundle container backgrounds locally and avoid remote requests. Generate the texture map from your inventory registry, then pass the config to `TextureProvider`:

```bash
pnpm add mc-assets   # optional peer dependency
```

```tsx
import { TextureProvider, InventoryOverlay } from 'minecraft-inventory'
import { localBundledTexturesConfig } from './bundledTexturesConfig'

// GUI container backgrounds (chest, furnace, etc.) load from bundled mc-assets;
// item/block textures still use the default remote URLs unless you override them.
<TextureProvider config={localBundledTexturesConfig}>
  <InventoryOverlay type="chest" ... />
</TextureProvider>
```

The generated file (`src/generated/localTextures.ts`) exports `bundledTextureMap`, `allTexturePaths`, and `allContainerPaths`. Use `createBundledTexturesConfig({ remoteFallback: true, bundledTextureMap? })` to get a config with `getGuiTextureUrl`, `setOverride(path, image)`, `clearOverrides()`, `setRemoteFallback(enabled)`, and `resetRenderedSlots()`. Pass short paths to `setOverride` (e.g. `gui/sprites/container/anvil/text_field_disabled.png`) — the version prefix exists only for the texture import generator. Call `resetRenderedSlots()` after multiple `setOverride` calls to invalidate cached textures so slots re-request them. `localBundledTexturesConfig` is the default instance. Re-run `pnpm gen:textures` after changing [inventory types](#adding-new-inventory-types).

---

## Connector

The connector is the bridge between the GUI and a Minecraft server or bot.

### Mineflayer connector

Use `createMineflayerConnector(bot)` to plug the GUI into a [mineflayer](https://github.com/PrismarineJS/mineflayer) bot. The connector turns slot clicks, drags, and drops into `bot.clickWindow()` (and plugin APIs for villager trades, enchantment table, anvil, beacon), and subscribes to mineflayer inventory events so the UI stays in sync.

**Example — overlay when a container opens:**

```tsx
import React, { useState, useEffect } from 'react'
import { createRoot } from 'react-dom/client'
import mineflayer from 'mineflayer'
import {
  InventoryProvider,
  ScaleProvider,
  TextureProvider,
  InventoryOverlay,
  createMineflayerConnector,
} from 'minecraft-inventory'

const bot = mineflayer.createBot({
  host: 'localhost',
  port: 25565,
  username: 'InventoryViewer',
})

function App() {
  const [connector, setConnector] = useState(null)
  const [windowType, setWindowType] = useState(null)
  const [open, setOpen] = useState(false)

  useEffect(() => {
    const onOpen = () => {
      setConnector(createMineflayerConnector(bot))
      setWindowType(bot.currentWindow?.type ?? 'generic_9x1')
      setOpen(true)
    }
    const onClose = () => {
      setOpen(false)
      setConnector(null)
    }

    bot.on('windowOpen', onOpen)
    bot.on('windowClose', onClose)
    return () => {
      bot.removeListener('windowOpen', onOpen)
      bot.removeListener('windowClose', onClose)
    }
  }, [])

  if (!open || !connector || !windowType) return null

  return (
    <TextureProvider>
      <ScaleProvider scale={2}>
        <InventoryProvider connector={connector}>
          <InventoryOverlay
            type={windowType}
            onClose={() => bot.closeWindow(bot.currentWindow)}
            showJEI
            jeiItems={[]}
          />
        </InventoryProvider>
      </ScaleProvider>
    </TextureProvider>
  )
}

createRoot(document.getElementById('root')).render(<App />)
```

**Hotbar “open inventory” button:** If you render a hotbar with the `container` option (e.g. mobile open-inventory button), the connector handles the `open-inventory` action: it calls `openPlayerInventory()`, which opens the player inventory GUI, or the ridden entity’s inventory (e.g. llama) when mounted. No extra wiring needed once the connector is passed to `InventoryProvider`.

### Demo connector (for local testing)

```ts
import { createDemoConnector } from 'minecraft-inventory'

const connector = createDemoConnector({
  windowType: 'crafting_table',
  windowTitle: 'Crafting',
  slots: [...],
  onAction: (entry) => {
    console.log(entry.description, entry.action)
  },
})

// Update slots programmatically:
connector.updateSlots(newSlots)
connector.setHeldItem({ type: 264, name: 'diamond', count: 1 })
connector.openWindow('furnace', 'Furnace', furnaceSlots)
connector.closeWindowExternal()
```

### Custom connector

Implement the `InventoryConnector` interface for any other backend:

```ts
import type { InventoryConnector } from 'minecraft-inventory'

const myConnector: InventoryConnector = {
  getWindowState: () => ({ windowId: 1, type: 'chest', slots: [...], heldItem: null }),
  getPlayerState: () => null,
  sendAction: async (action) => {
    // send action to server
    console.log('action', action)
  },
  closeWindow: () => {},
  subscribe: (listener) => {
    // call listener({ type: 'windowUpdate', state }) on changes
    return () => {} // cleanup
  },
}
```

---

## Keyboard Shortcuts (Desktop)

| Key | Action |
|---|---|
| Left click | Pick up all / place all |
| Right click | Pick up half / place one |
| Shift + Left click | Transfer to/from container |
| Double click | Collect all of same item type |
| 1–9 (while hovering) | Swap slot with hotbar slot N |
| Q (while hovering) | Drop one item |
| Ctrl+Q | Drop entire stack |
| Scroll up | Pick up one more (right-click equivalent) |
| Scroll down | Put one back |
| Esc | Close window (`onClose` callback) |

---

## Mobile Support

On touch devices, tapping a slot uses the mobile focus/swap flow. Long-pressing a populated slot with no held item opens a context menu. The menu appears to the side in landscape or below in portrait.

Context menu options:
- **Pick Half** — picks up half and highlights the selected slot
- **Pick Amount…** — `window.prompt` to enter a custom quantity, remembering the last value entered
- **Drop One** — drops a single item from the slot (same as `Q`)
- **Drop All** — drops the full stack from the slot (same as `Ctrl+Q`)
- **Cancel**

When you have a held item, tapping a slot places/transfers it (same as left-click on desktop).

---

## JEI — Item Browser

```tsx
import { JEI } from 'minecraft-inventory'

const items = [
  { type: 264, name: 'diamond', displayName: 'Diamond' },
  { type: 265, name: 'iron_ingot', displayName: 'Iron Ingot' },
  // ...
]

<JEI
  items={items}
  position="right"           // 'left' | 'right'
  onItemClick={(item) => {}} // left-click handler
  onItemMiddleClick={(item) => {}} // middle-click handler
/>
```

Use the search bar to filter by display name or item name. Scroll wheel or arrow buttons to paginate.

---

## Adding New Inventory Types

All inventory types live in a single registry file:

**`src/registry/inventories.ts`**

Add a new entry to the `inventoryDefinitions` object:

```ts
export const inventoryDefinitions: Record<string, InventoryTypeDefinition> = {
  // ... existing types ...

  my_custom_chest: {
    name: 'my_custom_chest',
    title: 'Custom Chest',
    backgroundTexture: 'gui/container/my_custom_chest',  // path under textures/
    backgroundWidth: 176,
    backgroundHeight: 166,
    includesPlayerInventory: true,
    includesHotbar: true,
    slots: [
      // Custom container slots (indices 0-N)
      ...gridSlots(0, 9, 3, 8, 18, 'container'),
      // Player inventory (adjust indices to match server protocol)
      ...playerInv(84, 27, 54),
    ],
  },
}
```

**Slot index conventions:**
- Container slots always start at `0`
- Player inventory and hotbar indices vary by window type (match the server protocol)
- Use `playerInv(yPos, invStartIndex, hotbarStartIndex)` for the standard 3×9 + hotbar layout

**Progress bars** (for furnaces, brewing stands, etc.):

```ts
progressBars: [
  {
    id: 'cook_arrow',
    x: 79, y: 34, width: 24, height: 16,
    direction: 'right',            // 'right' | 'up' | 'down' | 'left'
    textureX: 176, textureY: 14,   // source coords in background texture
    getValue: (props) => props.cookingProgress ?? 0,
    getMax: (props) => props.totalCookTime || 200,
  },
],
```

**Properties** (data slots from server):

```ts
properties: {
  cookingProgress: { dataSlot: 2, description: 'Cook progress (0-200)' },
  totalCookTime: { dataSlot: 3, description: 'Total cook time' },
},
```

**Registering at runtime** (optional, for plugins/mods):

```ts
import { registerInventoryType } from 'minecraft-inventory'

registerInventoryType({
  name: 'my_mod_inventory',
  title: 'Mod Inventory',
  backgroundTexture: 'gui/container/my_mod',
  backgroundWidth: 176,
  backgroundHeight: 166,
  slots: [...],
})
```

Then use it anywhere:

```tsx
<InventoryGUI type="my_mod_inventory" connector={connector} />
```

---

## ItemStack Type

```ts
interface ItemStack {
  type: number        // Numeric item ID
  name?: string       // Snake_case name, e.g. 'diamond_sword' (used for texture URL)
  count: number
  metadata?: number
  nbt?: Record<string, unknown>
  displayName?: string
  enchantments?: Array<{ name: string; level: number }>
  lore?: string[]
  durability?: number        // Current durability value
  maxDurability?: number     // Max durability (renders bar when < max)
  textureKey?: string        // Override texture path for getItemTextureUrl
  texture?: string | HTMLImageElement  // Direct texture (bypasses URL lookup)
  blockTexture?: BlockTextureRender   // Isometric block icon from face slices
  debugKey?: string
}
```

**Texture overrides (mineflayer `itemMapper`):**
- `texture: string` — URL or data URL, fetched and cached.
- `texture: HTMLImageElement` — Preloaded image, used directly.
- `blockTexture: { source, top, left, right }` — Composite an isometric block icon from three face slices. Each face has `slice: [x, y, w, h]` in source texture pixels. Uses a pool of aux canvases (not recreated per slot).

---

## Project Structure

```
src/
  index.tsx                  # Library entry point / exports
  types.ts                   # Core TypeScript types
  registry/
    index.ts                 # registerInventoryType / getInventoryType
    inventories.ts           # All built-in inventory type definitions ← ADD NEW TYPES HERE
  components/
    InventoryWindow/         # Main window renderer
    Slot/                    # Individual slot (click/drag/keyboard/mobile)
    ItemCanvas/              # Canvas-based item texture rendering
    Tooltip/                 # Cursor-following item tooltip
    JEI/                     # Item browser panel
    Hotbar/                  # Hotbar with active slot indicator
    HUD/                     # XP bar
    CursorItem/              # Floating item following mouse cursor
  context/
    InventoryContext.tsx      # Shared state (held item, hover, drag)
    ScaleContext.tsx          # CSS variable scale provider
    TextureContext.tsx        # Texture URL resolver
  connector/
    types.ts                 # InventoryConnector interface
    mineflayer.ts            # Mineflayer bot adapter
    demo.ts                  # Demo connector with action log
  hooks/
    useMobile.ts
    useKeyboardShortcuts.ts
  styles/
    tokens.css               # CSS custom property definitions
playground/
  src/
    App.tsx                  # Interactive playground / demo
    mockItems.ts             # Sample items for testing
```

---

## Contributing

### Running the playground

```bash
pnpm install
pnpm dev
# Opens at http://localhost:3200
```

### Adding a new inventory type

1. Open `src/registry/inventories.ts`
2. Add a new key to `inventoryDefinitions` (see template above)
3. Test it in the playground by adding it to `INVENTORY_TYPES` in `playground/src/App.tsx`
4. Submit a PR with the new type and sample mock slots in `playground/src/mockItems.ts`

### Slot index reference

Each Minecraft window type has a fixed slot layout defined by the server protocol. The indices must match `prismarine-windows` / mineflayer slot numbering. Cross-reference with:
- [`prismarine-windows`](https://github.com/PrismarineJS/prismarine-windows) for JS slot maps
- Minecraft source: `net/minecraft/world/inventory/` for the canonical layout

### Connector protocol

When implementing a custom connector, actions have these shapes:

| Action type | Fields |
|---|---|
| `click` | `slotIndex`, `button` ('left'/'right'/'middle'), `mode` ('normal'/'shift'/'double'/'number'/'drop'), `numberKey?` |
| `drop` | `slotIndex`, `all` (boolean) |
| `drag` | `slots` (number[]), `button` |
| `trade` | `tradeIndex` |
| `rename` | `text` |
| `enchant` | `enchantIndex` (0/1/2) |
| `beacon` | `primaryEffect`, `secondaryEffect` |
| `hotbar-swap` | `slotIndex`, `hotbarSlot` (0-8) |
| `close` | — |

---

## License

MIT
