import { HOTBAR_END_SLOT, HOTBAR_START_SLOT, OFF_HAND_SLOT } from '../utils/constants' import Module from '../utils/Module' import { ISlot } from '../utils/types' type Ingredient = ISlot[] type IRecipe = IRecipeCraftingShapeless | IRecipeCraftingShaped | IRecipeSpecial | IRecipeSmelting | IRecipeStoneCutting interface IRecipeCraftingShapeless { type: 'minecraft:crafting_shapeless' recipeId: string data: { group: string, ingredients: Ingredient[], result: ISlot, } } interface IRecipeCraftingShaped { type: 'minecraft:crafting_shaped' recipeId: string data: { group: string, width: number, height: number, ingredients: Ingredient[][], result: ISlot, } } interface IRecipeSpecial { type: string recipeId: string } interface IRecipeSmelting { type: 'minecraft:smelting' | 'minecraft:blasting' | 'minecraft:smoking' | 'minecraft:campfire_cooking' recipeId: string data: { group: string, ingredient: Ingredient, result: ISlot, experience: number, cookTime: number, } } interface IRecipeStoneCutting { type: 'minecraft:stonecutting' recipeId: string data: { group: string, ingredient: Ingredient, result: ISlot, } } interface IWindow { items?: ISlot[] closed?: true properties?: number[] } const NULL_SLOT: ISlot = { present: false, itemId: -1, itemCount: 0, nbtData: [] } export default class Inventory extends Module<{}> { private _recipes!: IRecipe[] public get recipes() { return this._recipes } private _windows = new Map() public get inventory() { return this._windows.get(0)?.items || [] } public get armor() { return { head: this.inventory[5], chest: this.inventory[6], legs: this.inventory[7], feet: this.inventory[8], } } private get hotbar() { return this.inventory.slice(HOTBAR_START_SLOT, HOTBAR_END_SLOT) } private get offItem() { return this.inventory[OFF_HAND_SLOT] } private _held = 0 private _onHeld = new Array<(slot: number) => void>() public get mainHand() { return this._held } public get mainItem() { return this.hotbar[this.mainHand] } private _actionCounter = 0 public click(windowId: number, slot: number, mouseButton: number, mode: number, item: ISlot) { this.client.write('window_click', { windowId, slot, mouseButton, action: this._actionCounter++, mode, item }) } public toHotbar(to: number, from: number, windowId = 0) { const inv = this._windows.get(windowId)?.items const item = inv ? inv[from] : NULL_SLOT this.click(windowId, from, to, 2, item) } public drop(from: number, windowId = 0, stack = false) { this.click(windowId, from, stack ? 1 : 0, 4, NULL_SLOT) } public setHeld(slotId: number) { this.client.write('held_item_slot', { slotId }) this._held = slotId this._onHeld.forEach(cb => cb(slotId)) } public registerHeld(cb: (slot: number) => void) { this._onHeld.push(cb) } public swapOffItem() { this.client.write('block_dig', { status: 6, location: { x: 0, y: 0, z: 0 }, face: 0 }) } public selectItem(slot: number, allowOff: boolean) { let hand = -1 if (slot === OFF_HAND_SLOT && allowOff) { hand = 1 } else { let hotSlot = slot if (hotSlot < HOTBAR_START_SLOT || hotSlot > HOTBAR_END_SLOT) { hotSlot = this.hotbar.findIndex(s => !s.present) if (hotSlot < 0) { hotSlot = this.mainHand + 1 % (HOTBAR_END_SLOT - HOTBAR_START_SLOT) } this.toHotbar(hotSlot, slot) } else { hotSlot -= HOTBAR_START_SLOT } if (hotSlot !== this.mainHand || Math.random() > 0.9) { // Force held randomlys this.setHeld(hotSlot) } hand = 0 } return hand } public useItem(slot: number, allowOff = true) { this.client.write('use_item', { hand: this.selectItem(slot, allowOff) }) } public findFirst(order: number[]) { return this.inventory.reduce((prev, slot, pos) => { const idx = order.indexOf(slot.itemId) return idx < 0 || (prev.value > -1 && prev.value <= idx) ? prev : { slot: pos, value: idx } }, { slot: -1, value: -1 }) } protected mount(): void { this.client.on('declare_recipes', packet => this._recipes = packet.recipes) this.client.on('window_items', packet => this.updateWindow(packet.windowId, { items: packet.items})) this.client.on('close_window', packet => this.updateWindow(packet.windowId, { closed: true })) this.client.on('craft_progress_bar', packet => { const window = this._windows.get(packet.windowId) || {} if (!window.properties) { window.properties = [] } window.properties[packet.property] = packet.value this._windows.set(packet.windowId, window) }) this.client.on('transaction', packet => { // TODO: action list if (!packet.accepted) { this.logger.warn('Conflict') this.client.write('transaction', packet) // NOTE: may conflict with proxy } }) this.client.on('set_slot', packet => { const window = this._windows.get(packet.windowId) || {} if (!window.items) { window.items = [] } window.items[packet.slot] = packet.item this._windows.set(packet.windowId, window) }) this.client.on('held_item_slot', packet => this._held = packet.slot) } protected getConf() { return {} } private updateWindow(windowId: number, data: IWindow) { const window = this._windows.get(windowId) || {} this._windows.set(windowId, {...window, ...data}) } // TODO: autodrop } // MAYBE: horse_window /* Action */ // TODO: Pick Item // TODO: craft // TODO: Log actions // TODO: Move inventory (equip armor)