import type { InventoryAction, InventoryWindowState, PlayerState, SlotState, ItemStack } from '../types' import type { InventoryConnector, ConnectorListener, ConnectorEvent } from './types' import { isItemEqual, getMaxStackSize } from '../utils/isItemEqual' export interface ActionLogEntry { id: number timestamp: number action: InventoryAction description: string } export interface DemoConnectorOptions { windowType: string windowTitle?: string slots?: SlotState[] playerInventory?: SlotState[] onAction?: (entry: ActionLogEntry) => void } let actionCounter = 0 function describeAction(action: InventoryAction): string { switch (action.type) { case 'click': return `${action.mode === 'shift' ? 'Shift-' : ''}${action.button === 'right' ? 'Right' : 'Left'} click slot ${action.slotIndex}${action.mode === 'number' ? ` (hotbar ${action.numberKey})` : ''}` case 'drop': return `Drop ${action.all ? 'all' : 'one'} from slot ${action.slotIndex}` case 'drag': return `Drag ${action.button} across slots: ${action.slots.join(', ')}` case 'close': return 'Close window' case 'trade': return `Trade at index ${action.tradeIndex}` case 'rename': return `Rename to "${action.text}"` case 'enchant': return `Select enchantment ${action.enchantIndex}` case 'beacon': return `Set beacon effects: ${action.primaryEffect} / ${action.secondaryEffect}` case 'hotbar-swap': return `Swap slot ${action.slotIndex} with hotbar ${action.hotbarSlot}` case 'hotbar-select': return `Select hotbar slot (index ${action.slotIndex})` default: return JSON.stringify(action) } } export function createDemoConnector(options: DemoConnectorOptions): InventoryConnector & { actionLog: ActionLogEntry[] updateSlots(slots: SlotState[]): void setHeldItem(item: ItemStack | null): void openWindow(type: string, title: string, slots: SlotState[]): void closeWindowExternal(): void } { const listeners = new Set() const actionLog: ActionLogEntry[] = [] let windowState: InventoryWindowState | null = { windowId: 1, type: options.windowType, title: options.windowTitle ?? options.windowType, slots: options.slots ?? [], heldItem: null, } let playerState: PlayerState = { activeHotbarSlot: 0, inventory: options.playerInventory ?? [], } function emit(event: ConnectorEvent) { listeners.forEach((l) => l(event)) } return { actionLog, getWindowState: () => windowState, getPlayerState: () => playerState, sendAction: (action: InventoryAction) => { const entry: ActionLogEntry = { id: ++actionCounter, timestamp: Date.now(), action, description: describeAction(action), } actionLog.unshift(entry) if (actionLog.length > 100) actionLog.splice(100) options.onAction?.(entry) if (action.type === 'hotbar-select') { if (action.slotIndex >= 36 && action.slotIndex <= 44) { playerState = { ...playerState, activeHotbarSlot: action.slotIndex - 36 } emit({ type: 'playerUpdate', state: playerState }) } return } // Demo: simulate simple click behavior if (action.type === 'click' && windowState) { const slots = [...windowState.slots] const slotState = slots.find((s) => s.index === action.slotIndex) const held = windowState.heldItem // Hotbar HUD: empty-hand click on main bar = select only (no pick) if ( windowState.type === 'hotbar' && action.button === 'left' && action.mode === 'normal' && !held && action.slotIndex >= 36 && action.slotIndex <= 44 ) { playerState = { ...playerState, activeHotbarSlot: action.slotIndex - 36 } emit({ type: 'playerUpdate', state: playerState }) return } if (action.button === 'left' && action.mode === 'normal') { if (held && slotState) { const idx = slots.indexOf(slotState) const existing = slotState.item if (!existing) { slots[idx] = { ...slotState, item: held } windowState = { ...windowState, slots, heldItem: null } } else if ( existing.type === held.type && (existing.name ?? '') === (held.name ?? '') && (existing.metadata ?? 0) === (held.metadata ?? 0) ) { // Same item type — combine stacks const maxStack = 64 const space = maxStack - existing.count if (space > 0) { const take = Math.min(held.count, space) slots[idx] = { ...slotState, item: { ...existing, count: existing.count + take } } const remain = held.count - take windowState = { ...windowState, slots, heldItem: remain > 0 ? { ...held, count: remain } : null } } else { // Stack full — swap slots[idx] = { ...slotState, item: held } windowState = { ...windowState, slots, heldItem: existing } } } else { // Different item types — swap slots[idx] = { ...slotState, item: held } windowState = { ...windowState, slots, heldItem: existing } } } else if (!held && slotState?.item) { const idx = slots.indexOf(slotState) slots[idx] = { ...slotState, item: null } windowState = { ...windowState, slots, heldItem: slotState.item } } } else if (action.button === 'right' && action.mode === 'normal') { if (!held && slotState?.item) { const item = slotState.item const half = Math.ceil(item.count / 2) const remain = item.count - half const idx = slots.indexOf(slotState) slots[idx] = { ...slotState, item: remain > 0 ? { ...item, count: remain } : null } windowState = { ...windowState, slots, heldItem: { ...item, count: half } } } else if (held && slotState) { const idx = slots.indexOf(slotState) const existing = slotState.item if (!existing || existing.type === held.type) { slots[idx] = { ...slotState, item: { ...held, count: (existing?.count ?? 0) + 1 }, } const newCount = held.count - 1 windowState = { ...windowState, slots, heldItem: newCount > 0 ? { ...held, count: newCount } : null, } } } } else if (action.mode === 'shift' && slotState?.item) { // Simulate shift-click: just log it } else if (action.mode === 'double') { // Double-click collect: pick up all matching items from other slots up to maxStack const held = windowState.heldItem if (held) { const maxStack = getMaxStackSize(held) let remaining = maxStack - held.count for (let i = 0; i < slots.length && remaining > 0; i++) { const s = slots[i] if (!s.item || !isItemEqual(s.item, held)) continue const take = Math.min(s.item.count, remaining) slots[i] = { ...s, item: s.item.count - take > 0 ? { ...s.item, count: s.item.count - take } : null } remaining -= take } windowState = { ...windowState, slots, heldItem: { ...held, count: maxStack - remaining } } } } emit({ type: 'windowUpdate', state: windowState }) } // Demo: simulate drag/spread behavior if (action.type === 'drag' && windowState && windowState.heldItem) { const held = windowState.heldItem const maxStack = getMaxStackSize(held) const slots = [...windowState.slots] if (action.button === 'left') { // Vanilla left-drag: distribute perSlot items evenly, remainder stays in cursor const compatibleSlots = action.slots.filter((idx) => { const existing = slots.find((s) => s.index === idx)?.item return !existing || isItemEqual(existing, held) }) if (compatibleSlots.length > 0) { const perSlot = Math.floor(held.count / compatibleSlots.length) // If perSlot=0 (more slots than items), nothing is distributed if (perSlot > 0) { let totalPlaced = 0 for (const idx of compatibleSlots) { const si = slots.findIndex((s) => s.index === idx) const existingCount = si >= 0 ? (slots[si].item?.count ?? 0) : 0 const add = Math.min(perSlot, maxStack - existingCount) totalPlaced += add const newCount = existingCount + add if (si >= 0) slots[si] = { ...slots[si], item: { ...held, count: newCount } } else slots.push({ index: idx, item: { ...held, count: newCount } }) } const remaining = held.count - totalPlaced windowState = { ...windowState, slots, heldItem: remaining > 0 ? { ...held, count: remaining } : null } } } } else { // Place 1 item per slot (right-click drag) let remaining = held.count for (const idx of action.slots) { if (remaining <= 0) break const si = slots.findIndex((s) => s.index === idx) const existing = si >= 0 ? slots[si].item : null if (existing && !isItemEqual(existing, held)) continue const existingCount = existing?.count ?? 0 const newCount = Math.min(existingCount + 1, maxStack) if (si >= 0) slots[si] = { ...slots[si], item: { ...held, count: newCount } } else slots.push({ index: idx, item: { ...held, count: newCount } }) remaining-- } windowState = { ...windowState, slots, heldItem: remaining > 0 ? { ...held, count: remaining } : null } } emit({ type: 'windowUpdate', state: windowState }) } if (action.type === 'drop' && windowState) { const slots = [...windowState.slots] const slotState = slots.find((s) => s.index === action.slotIndex) if (slotState?.item) { const idx = slots.indexOf(slotState) if (action.all) { slots[idx] = { ...slotState, item: null } } else { const count = slotState.item.count - 1 slots[idx] = { ...slotState, item: count > 0 ? { ...slotState.item, count } : null } } windowState = { ...windowState, slots } emit({ type: 'windowUpdate', state: windowState }) } } }, closeWindow: () => { windowState = null emit({ type: 'windowClose' }) }, openPlayerInventory: () => { // Demo: nothing to ride, just a no-op (player inventory lives in playerState) }, subscribe: (listener: ConnectorListener) => { listeners.add(listener) return () => listeners.delete(listener) }, updateSlots: (slots: SlotState[]) => { if (windowState) { windowState = { ...windowState, slots } emit({ type: 'windowUpdate', state: windowState }) } }, setHeldItem: (item: ItemStack | null) => { if (windowState) { windowState = { ...windowState, heldItem: item } emit({ type: 'heldItemChange', item }) } }, openWindow: (type: string, title: string, slots: SlotState[]) => { windowState = { windowId: ++actionCounter, type, title, slots, heldItem: null } emit({ type: 'windowOpen', state: windowState }) }, closeWindowExternal: () => { windowState = null emit({ type: 'windowClose' }) }, } }