const assert = require('assert') const EventEmitter = require('events').EventEmitter module.exports = (Item, registry) => { return class Window extends EventEmitter { constructor (id, type, title, slotCount, inventorySlotsRange = { start: 27, end: 62 }, craftingResultSlot = -1, requiresConfirmation = true) { super() this.id = id this.type = type this.title = title this.slots = new Array(slotCount).fill(null) this.inventoryStart = inventorySlotsRange.start this.inventoryEnd = inventorySlotsRange.end + 1 this.hotbarStart = this.inventoryEnd - 9 this.craftingResultSlot = craftingResultSlot this.requiresConfirmation = requiresConfirmation // in vanilla client, this is the item you are holding with the // mouse cursor this.selectedItem = null } acceptClick (click, gamemode = 0) { const { mode, slot, mouseButton } = click assert.ok( (mode >= 0 && mode <= 6) && (mouseButton >= 0 && mouseButton <= 8) && ((slot >= 0 && slot < this.inventoryEnd) || slot === -999 || (this.type === 'minecraft:inventory' && slot === 45)), 'invalid operation') switch (click.mode) { case 0: assert.ok(mouseButton <= 1, 'invalid operation') return this.mouseClick(click) case 1: assert.ok(mouseButton <= 1, 'invalid operation') return this.shiftClick(click) case 2: assert.ok(mouseButton <= 8, 'invalid operation') return this.numberClick(click) case 3: assert.ok(mouseButton === 2, 'invalid operation') return this.middleClick(click, gamemode) case 4: assert.ok(mouseButton <= 1, 'invalid operation') return this.dropClick(click) case 5: assert.ok([1, 5, 9, 2, 6, 10].includes(mouseButton), 'invalid operation') return this.dragClick(click, gamemode) case 6: assert.ok(mouseButton === 0, 'invalid operation') return this.doubleClick(click) } } mouseClick (click) { if (click.slot === -999) { this.dropSelectedItem(click.mouseButton === 0) } else { let { item } = click if (click.mouseButton === 0) { // left click if (item && this.selectedItem) { if (Item.equal(item, this.selectedItem, false)) { if (click.slot === this.craftingResultSlot) { const maxTransferrable = this.selectedItem.stackSize - this.selectedItem.count if (item.count > maxTransferrable) { this.selectedItem.count += maxTransferrable item.count -= maxTransferrable } else if (item.count <= maxTransferrable) { this.selectedItem.count += item.count this.updateSlot(item.slot, null) } } else { this.fillSlotWithSelectedItem(item, true) } } else { this.swapSelectedItem(click.slot, item) } return [click.slot] } else if (this.selectedItem || item) { this.swapSelectedItem(click.slot, item) return [click.slot] } } else if (click.mouseButton === 1) { // right click if (this.selectedItem) { if (item) { if (Item.equal(item, this.selectedItem, false)) { this.fillSlotWithSelectedItem(item, false) } else { this.swapSelectedItem(click.slot, item) } } else { item = new Item(this.selectedItem.type, 0, this.selectedItem.metadata, this.selectedItem.nbt) this.updateSlot(click.slot, item) this.fillSlotWithSelectedItem(item, false) } return [click.slot] } else if (item && click.slot !== this.craftingResultSlot) { this.splitSlot(item) return [click.slot] } } } return [] } shiftClick (click) { const { item } = click if (!item) return if (this.type === 'minecraft:inventory') { if (click.slot < this.inventoryStart) { this.fillAndDump(item, this.inventoryStart, this.inventoryEnd, click.slot === this.craftingResultSlot) } else { if (click.slot >= this.inventoryStart && click.slot < this.inventoryEnd - 10) { this.fillAndDump(item, this.hotbarStart, this.inventoryEnd) } else { this.fillAndDump(item, this.inventoryStart, this.inventoryEnd) } } } else { if (click.slot < this.inventoryStart) { this.fillAndDump(item, this.inventoryStart, this.inventoryEnd, this.craftingResultSlot === -1 || click.slot === this.craftingResultSlot) } else { this.fillAndDump(item, 0, this.inventoryStart - 1) } } } numberClick (click) { if (this.selectedItem) return const { item } = click const hotbarSlot = this.hotbarStart + click.mouseButton const itemAtHotbarSlot = this.slots[hotbarSlot] if (Item.equal(item, itemAtHotbarSlot) && item?.slot === hotbarSlot) return if (item) { if (itemAtHotbarSlot) { if ((this.type === 'minecraft:inventory' || registry.version['>=']('1.9')) && click.slot !== this.craftingResultSlot) { this.updateSlot(click.slot, itemAtHotbarSlot) this.updateSlot(hotbarSlot, item) } else { this.dumpItem(itemAtHotbarSlot, this.hotbarStart, this.inventoryEnd) if (this.slots[hotbarSlot]) { this.dumpItem(itemAtHotbarSlot, this.inventoryStart, this.hotbarStart - 1) } if (this.slots[hotbarSlot] === null) { this.updateSlot(item.slot, null) this.updateSlot(hotbarSlot, item) let slots = this.findItemsRange(this.hotbarStart, this.inventoryEnd, itemAtHotbarSlot.type, itemAtHotbarSlot.metadata, true, itemAtHotbarSlot.nbt) slots.push(...this.findItemsRange(this.inventoryStart, this.hotbarStart - 1, itemAtHotbarSlot.type, itemAtHotbarSlot.metadata, true, itemAtHotbarSlot.nbt)) slots = slots.filter(slot => slot.slot !== itemAtHotbarSlot.slot) this.fillSlotsWithItem(slots, itemAtHotbarSlot) } } } else { this.updateSlot(item.slot, null) this.updateSlot(hotbarSlot, item) } } else if (itemAtHotbarSlot && click.slot !== this.craftingResultSlot) { this.updateSlot(click.slot, itemAtHotbarSlot) this.updateSlot(hotbarSlot, null) } } middleClick (click, gamemode) { if (this.selectedItem) return [] const { item } = click if (gamemode === 1 && item) { this.selectedItem = new Item(item.type, item.stackSize, item.metadata, item.nbt) } return [] } dropClick (click) { const { item } = click if (this.selectedItem || item === null) return [] if (click.mouseButton === 0) { if (--click.item.count === 0) this.updateSlot(click.slot, null) return [click.slot] } else if (click.mouseButton === 1) { this.updateSlot(click.slot, null) return [click.slot] } } dragClick (click, gamemode) { // unimplemented assert.ok(false, 'unimplemented') } doubleClick (click) { // unimplemented assert.ok(false, 'unimplemented') } acceptOutsideWindowClick = this.acceptClick acceptInventoryClick = this.acceptClick acceptNonInventorySwapAreaClick = this.acceptClick acceptSwapAreaLeftClick = this.acceptClick acceptSwapAreaRightClick = this.acceptClick acceptCraftingClick = this.acceptClick fillAndDump (item, start, end, lastToFirst = false) { this.fillSlotsWithItem(this.findItemsRange(start, end, item.type, item.metadata, true, item.nbt, true), item, lastToFirst) if (this.slots[item.slot]) { this.dumpItem(item, start, end, lastToFirst) } } fillSlotsWithItem (slots, item, lastToFirst = false) { while (slots.length && item.count) { this.fillSlotWithItem(lastToFirst ? slots.pop() : slots.shift(), item) } } fillSlotWithItem (itemToFill, itemToTake) { const newCount = itemToFill.count + itemToTake.count const leftover = newCount - itemToFill.stackSize if (leftover <= 0) { itemToFill.count = newCount itemToTake.count = 0 this.updateSlot(itemToTake.slot, null) } else { itemToFill.count = itemToFill.stackSize itemToTake.count = leftover } } fillSlotWithSelectedItem (item, untilFull) { if (untilFull) { const newCount = item.count + this.selectedItem.count const leftover = newCount - item.stackSize if (leftover <= 0) { item.count = newCount this.selectedItem = null } else { item.count = item.stackSize this.selectedItem.count = leftover } } else { if (item.count + 1 <= item.stackSize) { item.count++ if (--this.selectedItem.count === 0) this.selectedItem = null } } } dumpItem (item, start, end, lastToFirst = false) { const emptySlot = lastToFirst ? this.lastEmptySlotRange(start, end) : this.firstEmptySlotRange(start, end) if (emptySlot !== null && emptySlot !== this.craftingResultSlot) { const slot = item.slot this.updateSlot(emptySlot, item) this.updateSlot(slot, null) } } splitSlot (item) { if (!item) return this.selectedItem = new Item(item.type, Math.ceil(item.count / 2), item.metadata, item.nbt) item.count -= this.selectedItem.count if (item.count === 0) this.updateSlot(item.slot, null) } swapSelectedItem (slot, item) { this.updateSlot(slot, this.selectedItem) this.selectedItem = item } dropSelectedItem (untilEmpty) { if (untilEmpty || --this.selectedItem.count === 0) this.selectedItem = null } updateSlot (slot, newItem) { if (newItem) newItem.slot = slot const oldItem = this.slots[slot] this.slots[slot] = newItem this.emit('updateSlot', slot, oldItem, newItem) this.emit(`updateSlot:${slot}`, oldItem, newItem) } findItemsRange (start, end, itemType, metadata, notFull, nbt, withoutCraftResultSlot = false) { const items = [] while (start < end) { const item = this.findItemRange(start, end, itemType, metadata, notFull, nbt, withoutCraftResultSlot) if (!item) break start = item.slot + 1 items.push(item) } return items } findItemRange (start, end, itemType, metadata, notFull, nbt, withoutCraftResultSlot = false) { assert.notStrictEqual(itemType, null) for (let i = start; i < end; ++i) { const item = this.slots[i] if ( item && itemType === item.type && (metadata == null || metadata === item.metadata) && (!notFull || item.count < item.stackSize) && (nbt == null || JSON.stringify(nbt) === JSON.stringify(item.nbt)) && !(item.slot === this.craftingResultSlot && withoutCraftResultSlot)) { return item } } return null } findItemRangeName (start, end, itemName, metadata, notFull) { assert.notStrictEqual(itemName, null) for (let i = start; i < end; ++i) { const item = this.slots[i] if (item && itemName === item.name && (metadata == null || metadata === item.metadata) && (!notFull || item.count < item.stackSize)) { return item } } return null } findInventoryItem (item, metadata, notFull) { assert(typeof item === 'number' || typeof item === 'string' || typeof item === 'undefined', 'No valid type given') return typeof item === 'number' ? this.findItemRange(this.inventoryStart, this.inventoryEnd, item, metadata, notFull) : this.findItemRangeName(this.inventoryStart, this.inventoryEnd, item, metadata, notFull) } findContainerItem (item, metadata, notFull) { assert(typeof item === 'number' || typeof item === 'string' || typeof item === 'undefined', 'No valid type given') return typeof item === 'number' ? this.findItemRange(0, this.inventoryStart, item, metadata, notFull) : this.findItemRangeName(0, this.inventoryStart, item, metadata, notFull) } firstEmptySlotRange (start, end) { for (let i = start; i < end; ++i) { if (this.slots[i] === null) return i } return null } lastEmptySlotRange (start, end) { for (let i = end; i >= start; i--) { if (this.slots[i] === null) return i } return null } firstEmptyHotbarSlot () { return this.firstEmptySlotRange(this.hotbarStart, this.inventoryEnd) } firstEmptyContainerSlot () { return this.firstEmptySlotRange(0, this.inventoryStart) } firstEmptyInventorySlot (hotbarFirst = true) { if (hotbarFirst) { const slot = this.firstEmptyHotbarSlot() if (slot !== null) return slot } return this.firstEmptySlotRange(this.inventoryStart, this.inventoryEnd) } sumRange (start, end) { let sum = 0 for (let i = start; i < end; i++) { const item = this.slots[i] if (item) sum += item.count } return sum } countRange (start, end, itemType, metadata) { let sum = 0 for (let i = start; i < end; ++i) { const item = this.slots[i] if (item && itemType === item.type && (metadata == null || item.metadata === metadata)) { sum += item.count } } return sum } itemsRange (start, end) { const results = [] for (let i = start; i < end; ++i) { const item = this.slots[i] if (item) results.push(item) } return results } count (itemType, metadata) { itemType = parseInt(itemType, 10) // allow input to be string return this.countRange(this.inventoryStart, this.inventoryEnd, itemType, metadata) } items () { return this.itemsRange(this.inventoryStart, this.inventoryEnd) } containerCount (itemType, metadata) { itemType = parseInt(itemType, 10) // allow input to be string return this.countRange(0, this.inventoryStart, itemType, metadata) } containerItems () { return this.itemsRange(0, this.inventoryStart) } emptySlotCount () { let count = 0 for (let i = this.inventoryStart; i < this.inventoryEnd; ++i) { if (this.slots[i] === null) count += 1 } return count } transactionRequiresConfirmation (click) { return this.requiresConfirmation } clear (blockId, count) { let clearedCount = 0 const iterLoop = (currSlot) => { if (!currSlot || (blockId && currSlot.type !== blockId)) return false const blocksNeeded = count - clearedCount if (count && currSlot.count > blocksNeeded) { // stack is bigger then needed clearedCount += blocksNeeded this.updateSlot(currSlot.slot, new Item(blockId, currSlot.count - blocksNeeded, currSlot.metadata, currSlot.nbt)) } else { // stack is just big enough or too little items to finish counter clearedCount += currSlot.count this.updateSlot(currSlot.slot, null) } if (count === clearedCount) return true // we have enough items return false } for (let i = this.inventoryEnd; i > this.hotbarStart - 1; i--) { if (iterLoop(this.slots[i])) break } if (clearedCount !== count) { for (let i = this.inventoryStart; i < this.hotbarStart; i++) { if (iterLoop(this.slots[i])) break } } return clearedCount } } }