UNPKG

inventory-window

Version:

a GUI window for interactively manipulating an inventory of itempiles

502 lines (437 loc) 17.8 kB
'use strict'; const EventEmitter = require('events').EventEmitter; const ever = require('ever'); const createTooltip = require('ftooltip'); const CubeIcon = require('cube-icon'); const touchup = require('touchup'); class InventoryWindow extends EventEmitter { // moved to global.InventoryWindow_ instead of class variable, since this module // might be included multiple times, creating multiple class variables, but we // want them to be shared across _all_ instances, so you can drop between any // inventory-window. //this.heldItemPile = undefined //this.heldNode = undefined //this.mouseButtonDown = undefined //this.resolvedImageURLs = {} constructor(opts) { super(); if (!opts) opts = {}; this.inventory = opts.inventory; if (!this.inventory) throw new Error('inventory-window requires "inventory" option set to Inventory instance'); this.linkedInventory = opts.linkedInventory; this.getTexture = opts.getTexture; if (!this.getTexture) this.getTexture = InventoryWindow.defaultGetTexture; if (!this.getTexture) this.getTexture = global.InventoryWindow_defaultGetTexture; this.registry = opts.registry; if (!this.getTexture && !this.registry) { throw new Error('inventory-window: required "getTexture" or "registry" option missing'); } this.getMaxDamage = opts.getMaxDamage; if (!this.getMaxDamage) this.getMaxDamage = InventoryWindow.defaultGetMaxDamage; if (!this.getMaxDamage) this.getMaxDamage = global.InventoryWindow_defaultGetMaxDamage; this.inventorySize = opts.inventorySize; if (this.inventorySize === undefined) this.inventorySize = this.inventory.size(); this.width = opts.width; if (this.width === undefined) this.width = this.inventory.width; this.textureScale = opts.textureScale !== undefined ? opts.textureScale : 5; this.textureScaleAlgorithm = 'nearest-neighbor'; this.textureSrcPx = opts.textureSrcPx !== undefined ? opts.textureSrcPx : 16; this.textureSize = opts.textureSize !== undefined ? opts.textureSize : (this.textureSrcPx * this.textureScale); this.getTooltip = opts.getTooltip; if (!this.getTooltip) this.getTooltip = InventoryWindow.defaultGetTooltip; if (!this.getTooltip) this.getTooltip = global.InventoryWindow_defaultGetTooltip; this.tooltips = opts.tooltips !== undefined ? opts.tooltips : true; this.borderSize = opts.borderSize !== undefined ? opts.borderSize : 4; this.progressThickness = opts.progressThickness !== undefined ? opts.progressThickness : 10; this.secondaryMouseButton = opts.secondaryMouseButton !== undefined ? opts.secondaryMouseButton : 2; this.allowDrop = opts.allowDrop !== undefined ? opts.allowDrop : true; this.allowPickup = opts.allowPickup !== undefined ? opts.allowPickup : true; this.allowDragPaint = opts.allowDragPaint !== undefined ? opts.allowDragPaint : true; this.progressColorsThresholds = opts.progressColorsThresholds !== undefined ? opts.progressColorsThresholds : [0.20, 0.40, Infinity]; this.progressColors = opts.progressColors !== undefined ? opts.progressColors : ['red', 'orange', 'green']; this.slotNodes = []; this.container = undefined; this.selectedIndex = undefined; // no selection this.enable(); } enable() { if (global.document) { ever(document).on('mousemove', (ev) => { if (!global.InventoryWindow_heldNode) return; this.positionAtMouse(global.InventoryWindow_heldNode, ev); }); ever(document).on('mouseup', (ev) => { global.InventoryWindow_mouseButtonDown = undefined; }); } this.inventory.on('changed', () => { this.refresh(); }); }; createContainer() { if (!global.document) return; let container = document.createElement('div'); for (let i = 0; i < this.inventorySize; ++i) { const slotItem = this.inventory.get(i) const node = this.createSlotNode(slotItem); this.setBorderStyle(node, i); this.bindSlotNodeEvent(node, i); this.slotNodes.push(node); container.appendChild(node); } const widthpx = this.width * (this.textureSize + this.borderSize * 2) + 2 * this.borderSize; container.setAttribute('style', ` display: block; float: left; width: ${widthpx}px; -moz-user-select: none; -webkit-user-select: none; -ms-user-select: none; `); this.container = container; return this.container; } bindSlotNodeEvent(node, index) { ever(node).on('mousedown', (ev) => { this.clickSlot(index, ev); }); ever(node).on('mouseover', (ev) => { if (!this.allowDragPaint) return; if (!this.allowDrop) return; if (!global.InventoryWindow_heldItemPile) return; if (global.InventoryWindow_mouseButtonDown !== this.secondaryMouseButton) return; // 'drag paint' mode, distributing items as mouseover without clicking this.dropOneHeld(index); this.createHeldNode(global.InventoryWindow_heldItemPile, ev); this.refreshSlotNode(index); // TODO: support left-click drag paint = evenly redistribute // (vs right-click = drop only one item) }); } createSlotNode(itemPile) { const div = document.createElement('div'); div.setAttribute('style', ` display: inline-block; float: inherit; margin: 0; padding: 0; width: ${this.textureSize}px; height: ${this.textureSize}px; font-size: 20pt; background-size: 100% auto; image-rendering: -moz-crisp-edges; image-rendering: -o-crisp-edges; image-rendering: -webkit-optimize-contrast; image-rendering: crisp-edges; -ms-interpolation-mode: nearest-neighbor; `); // set image and text this.populateSlotNode(div, itemPile); return div; } populateSlotNode(div, itemPile, isSelected) { let src = undefined; let text = ''; let progress = undefined; let progressColor = undefined; if (itemPile !== undefined) { if (this.registry !== undefined) { src = this.registry.getItemPileTexture(itemPile); } else if (this.getTexture !== undefined) { src = this.getTexture(itemPile); } else { throw new Error('inventory-window textures not specified, set global.InventoryWindow_defaultGetTexture or pass "getTexture" or "registry" option'); } //text = this.getTextOverlay this.inventory.slot text = itemPile.count; if (text === 1) text = ''; if (text === Infinity) text = '\u221e'; if (itemPile.tags !== undefined && itemPile.tags.damage !== undefined) { let maxDamage; if (this.registry !== undefined) { maxDamage = this.registry.getItemProps(itemPile.item).maxDamage; } else if (this.getMaxDamage !== undefined) { maxDamage = this.getMaxDamage(itemPile); } else { maxDamage = 100; } progress = (maxDamage - itemPile.tags.damage) / maxDamage; progressColor = this.getProgressBarColor(progress); } } function setImage(src) { let newImage; if (typeof src === 'string') { // simple image newImage = 'url(' + src + ')'; } else { newImage = ''; // clear // note: might be 3d cube set below } // update image, but only if changed to prevent flickering if (global.InventoryWindow_resolvedImageURLs === undefined) { global.InventoryWindow_resolvedImageURLs = {}; } if (global.InventoryWindow_resolvedImageURLs[newImage] !== div.style.backgroundImage) { div.style.backgroundImage = newImage; // wrinkle: if the URL may not be fully resolved (relative path, ../, etc.), // but setting backgroundImage resolves it, so it won't always match what we // set it to -- to fix this, cache the result for comparison next time global.InventoryWindow_resolvedImageURLs[newImage] = div.style.backgroundImage; } } if (this.textureScaleAlgorithm !== undefined && typeof src === 'string') { // cache scaled images if (global.InventoryWindow_cachedScaledImages === undefined) { global.InventoryWindow_cachedScaledImages = {}; } if (global.InventoryWindow_cachedScaledImages[src]) { setImage(global.InventoryWindow_cachedScaledImages[src]); } else { // generate scaled image, requires async callback let img = new Image(); img.onload = () => { const scaled = touchup.scale(img, this.textureScale, this.textureScale, this.textureScaleAlgorithm); global.InventoryWindow_cachedScaledImages[src] = scaled; setImage(scaled); }; img.src = src; } } else { // unscaled image setImage(src); } // 3D cube node (for blocks) let cubeNode = div.children[0]; if (!cubeNode) { cubeNode = document.createElement('div'); cubeNode.setAttribute('style', 'position: relative; z-index: 0;'); div.appendChild(cubeNode); } while(cubeNode.firstChild) { cubeNode.removeChild(cubeNode.firstChild); } if (Array.isArray(src) || typeof(src) === 'object') { // 3d cube const cube = CubeIcon({images:src}); cubeNode.appendChild(cube.container); } // textual count let textBox = div.children[1]; if (!textBox) { textBox = document.createElement('div'); textBox.setAttribute('style', 'position: absolute; text-shadow: 1px 1px #eee, -1px -1px #333;'); div.appendChild(textBox); } if (textBox.textContent !== text) { textBox.textContent = text; } // progress bar let progressNode = div.children[2]; if (!progressNode) { progressNode = document.createElement('div'); progressNode.setAttribute('style', ` width: 0%; top: ${this.textureSize - this.borderSize * 2}px; position: relative; visibility: hidden; `); div.appendChild(progressNode); } if (progressColor !== undefined) { progressNode.style.borderTop = ` ${this.progressThickness}px solid ${progressColor}`; } if (progress !== undefined) { progressNode.style.width = (progress * 100) + '%'; } progressNode.style.visibility = progress !== undefined ? '' : 'hidden'; // tooltip if (this.tooltips) { let tooltipNode = div.children[3]; if (!tooltipNode) { tooltipNode = document.createTextNode('not set'); let tooltip = createTooltip(div, tooltipNode); div.appendChild(tooltip.div); } let tooltipText; if (itemPile) { if (this.registry) { tooltipText = this.registry.getItemDisplayName(itemPile.item); } else if (this.getTooltip) { tooltipText = this.getTooltip(itemPile); } } else { tooltipText = ''; } tooltipNode.textContent = tooltipText; } } getProgressBarColor(progress) { for (let i = 0; i < this.progressColorsThresholds.length; ++i) { const threshold = this.progressColorsThresholds.length[i]; if (progress <= threshold) { return this.progressColors[i]; } return this.progressColors.slice(-1)[0]; // default to last } } setBorderStyle(node, index) { // based on http://coffeescript.org // a // b Math.floor(a / b) // a %% b (a % b + b) % b // TODO: refactor function integer_division(a, b) { return Math.floor(a / b); } function true_modulo(a, b) { return (a % b + b) % b; } const x = true_modulo(index, this.width); const y = integer_division(index, this.width); const height = this.inventorySize / this.width; let kind; if (index === this.selectedIndex) { kind = 'dotted'; } else { kind = 'solid'; } node.style.border = `${this.borderSize}px ${kind} black` if (y === 0) node.style.borderTop = `${this.borderSize * 2}px ${kind} black`; if (y === height - 1) node.style.borderBottom = `${this.borderSize * 2}px ${kind} black`; if (x === 0) node.style.borderLeft = `${this.borderSize * 2}px ${kind} black`; if (x === this.width - 1) node.style.borderRight = `${this.borderSize * 2}px ${kind} black`; } setSelected(index) { this.selectedIndex = index; this.refresh(); // TODO: selective refresh? } getSelected(index) { return this.selectedIndex; } refreshSlotNode(index) { this.populateSlotNode(this.slotNodes[index], this.inventory.get(index)); this.setBorderStyle(this.slotNodes[index], index); } refresh() { for (let i = 0; i < this.inventorySize; ++i) { this.refreshSlotNode(i); } } positionAtMouse(node, mouseEvent) { let x = mouseEvent.x !== undefined ? mouseEvent.x : mouseEvent.clientX let y = mouseEvent.y !== undefined ? mouseEvent.y : mouseEvent.clientY; x -= this.textureSize / 2; y -= this.textureSize / 2; node.style.left = x + 'px'; node.style.top = y + 'px'; } createHeldNode(itemPile, ev) { if (global.InventoryWindow_heldNode) this.removeHeldNode(); if (!itemPile || itemPile.count === 0) { global.InventoryWindow_heldItemPile = undefined; return; } global.InventoryWindow_heldItemPile = itemPile; global.InventoryWindow_heldNode = this.createSlotNode(global.InventoryWindow_heldItemPile); global.InventoryWindow_heldNode.setAttribute('style', global.InventoryWindow_heldNode.getAttribute('style') + ` position: absolute; user-select: none; -moz-user-select: none; -webkit-user-select: none; pointer-events: none; z-index: 10; `); this.positionAtMouse(global.InventoryWindow_heldNode, ev); document.body.appendChild(global.InventoryWindow_heldNode); } removeHeldNode() { global.InventoryWindow_heldNode.parentNode.removeChild(global.InventoryWindow_heldNode); global.InventoryWindow_heldNode = undefined; global.InventoryWindow_heldItemPile = undefined; } dropOneHeld(index) { if (this.inventory.get(index)) { // drop one, but try to merge with existing let oneHeld = global.InventoryWindow_heldItemPile.splitPile(1); if (this.inventory.get(index).mergePile(oneHeld) === false) { // could not merge, so swap global.InventoryWindow_heldItemPile.increase(1); let tmp = global.InventoryWindow_heldItemPile; global.InventoryWindow_heldItemPile = this.inventory.get(index); this.inventory.set(index, tmp); } else { this.inventory.changed(); } } else { // drop on empty slot this.inventory.set(index, global.InventoryWindow_heldItemPile.splitPile(1)); } } clickSlot(index, ev) { let itemPile = this.inventory.get(index); console.log('clickSlot',index,itemPile); global.InventoryWindow_mouseButtonDown = ev.button; let shiftDown = ev.shiftKey; if (ev.button !== this.secondaryMouseButton) { // left click: whole pile if (!global.InventoryWindow_heldItemPile || !this.allowDrop) { // pickup whole pile if (!this.allowPickup) return; if (global.InventoryWindow_heldItemPile) { // tried to drop on pickup-only inventory, so merge into held inventory instead if (this.inventory.get(index) !== undefined) { if (!global.InventoryWindow_heldItemPile.canPileWith(this.inventory.get(index))) return; global.InventoryWindow_heldItemPile.mergePile(this.inventory.get(index)); } } else { if (!shiftDown) { // simply picking up the whole pile global.InventoryWindow_heldItemPile = this.inventory.get(index); this.inventory.set(index, undefined); } else if (this.linkedInventory && this.inventory.get(index) !== undefined) { // shift-click: transfer to linked inventory this.linkedInventory.give(this.inventory.get(index)); if (this.inventory.get(index).count === 0) { this.inventory.set(index, undefined); } this.inventory.changed(); // update source, might not have transferred all of the pile } } this.emit('pickup'); // TODO: event data? index, item? cancelable? } else { // drop whole pile if (this.inventory.get(index)) { // try to merge piles dropped on each other if (this.inventory.get(index).mergePile(global.InventoryWindow_heldItemPile) === false) { // cannot pile together; swap dropped/held let tmp = global.InventoryWindow_heldItemPile; global.InventoryWindow_heldItemPile = this.inventory.get(index); this.inventory.set(index, tmp); } else { this.inventory.changed(); } } else { // fill entire slot this.inventory.set(index, global.InventoryWindow_heldItemPile); global.InventoryWindow_heldItemPile = undefined; } } } else { // right-click: half/one if (!global.InventoryWindow_heldItemPile) { // pickup half if (!this.allowPickup) return; if (this.inventory.get(index) !== undefined) { global.InventoryWindow_heldItemPile = this.inventory.get(index).splitPile(0.5); } else { global.InventoryWindow_heldItemPile = undefined; } if (this.inventory.get(index) && this.inventory.get(index).count == 0) { this.inventory.set(index, undefined ); } this.inventory.changed(); this.emit('pickup'); // TODO: event data? index, item? cancelable? } else { if (!this.allowDrop) return; this.dropOneHeld(index); } } this.createHeldNode(global.InventoryWindow_heldItemPile, ev); this.refreshSlotNode(index); } } module.exports = InventoryWindow;