201 lines
6.0 KiB
TypeScript
201 lines
6.0 KiB
TypeScript
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<number, IWindow>()
|
|
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
|
|
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
|
|
}
|
|
|
|
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)
|
|
}
|
|
})
|
|
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)
|