UNPKG

hackedvoxels-inventory-window

Version:

a GUI window for interactively manipulating an inventory of itempiles

399 lines (336 loc) 14.1 kB
EventEmitter = (require 'events').EventEmitter ever = require 'ever' createTooltip = require 'ftooltip' CubeIcon = require 'cube-icon' touchup = require 'touchup' module.exports = 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. #@heldItemPile = undefined #@heldNode = undefined #@mouseButtonDown = undefined #@resolvedImageURLs = {} constructor: (opts) -> opts ?= {} @inventory = opts.inventory ? throw 'inventory-window requires "inventory" option set to Inventory instance' @linkedInventory = opts.linkedInventory @getTexture = opts.getTexture ? InventoryWindow.defaultGetTexture ? global.InventoryWindow_defaultGetTexture @registry = opts.registry if (!@getTexture? && !@registry?) throw 'inventory-window: required "getTexture" or "registry" option missing' @getMaxDamage = opts.getMaxDamage ? InventoryWindow.defaultGetMaxDamage ? global.InventoryWindow_defaultGetMaxDamage @inventorySize = opts.inventorySize ? @inventory.size() @width = opts.width ? @inventory.width @textureScale = opts.textureScale ? 5 @textureScaleAlgorithm = 'nearest-neighbor' @textureSrcPx = opts.textureSrcPx ? 16 @textureSize = opts.textureSize ? (@textureSrcPx * @textureScale) @getTooltip = opts.getTooltip ? InventoryWindow.defaultGetTooltip ? global.InventoryWindow_defaultGetTooltip @tooltips = opts.tooltips ? true @borderSize = opts.borderSize ? 4 @progressThickness = opts.progressThickness ? 10 @secondaryMouseButton = opts.secondaryMouseButton ? 2 @allowDrop = opts.allowDrop ? true @allowPickup = opts.allowPickup ? true @allowDragPaint = opts.allowDragPaint ? true @progressColorsThresholds = opts.progressColorsThresholds ?= [0.20, 0.40, Infinity] @progressColors = opts.progressColors ?= ['red', 'orange', 'green'] @slotNodes = [] @container = undefined @selectedIndex = undefined # no selection @enable() enable: () -> if document? ever(document).on 'mousemove', (ev) => return if not global.InventoryWindow_heldNode @positionAtMouse global.InventoryWindow_heldNode, ev ever(document).on 'mouseup', (ev) => global.InventoryWindow_mouseButtonDown = undefined @inventory.on 'changed', () => @refresh() createContainer: () -> return if not document? container = document.createElement 'div' for i in [0...@inventorySize] slotItem = @inventory.get(i) node = @createSlotNode(slotItem) @setBorderStyle node, i @bindSlotNodeEvent node, i @slotNodes.push node container.appendChild node widthpx = @width * (@textureSize + @borderSize * 2) + 2 * @borderSize container.setAttribute 'style', " display: block; float: left; width: #{widthpx}px; -moz-user-select: none; -webkit-user-select: none; -ms-user-select: none; " @container = container bindSlotNodeEvent: (node, index) -> ever(node).on 'mousedown', (ev) => @clickSlot index, ev ever(node).on 'mouseover', (ev) => return if not @allowDragPaint return if not @allowDrop return if not global.InventoryWindow_heldItemPile? return if global.InventoryWindow_mouseButtonDown != @secondaryMouseButton # 'drag paint' mode, distributing items as mouseover without clicking @dropOneHeld(index) @createHeldNode global.InventoryWindow_heldItemPile, ev @refreshSlotNode index # TODO: support left-click drag paint = evenly redistribute # (vs right-click = drop only one item) createSlotNode: (itemPile) -> div = document.createElement 'div' div.setAttribute 'style', " display: inline-block; float: inherit; margin: 0; padding: 0; width: #{@textureSize}px; height: #{@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 @populateSlotNode div, itemPile div populateSlotNode: (div, itemPile, isSelected) -> src = undefined text = '' progress = undefined progressColor = undefined if itemPile? if @registry? src = @registry.getItemPileTexture itemPile else if @getTexture? src = @getTexture itemPile else throw 'inventory-window textures not specified, set global.InventoryWindow_defaultGetTexture or pass "getTexture" or "registry" option' #text = @getTextOverlay @inventory.slot text = itemPile.count text = '' if text == 1 text = '\u221e' if text == Infinity if itemPile.tags?.damage? if @registry? maxDamage = @registry.getItemProps(itemPile.item).maxDamage else if @getMaxDamage? maxDamage = @getMaxDamage(itemPile) else maxDamage = 100 progress = (maxDamage - itemPile.tags.damage) / maxDamage progressColor = @getProgressBarColor(progress) setImage = (src) -> 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 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 @textureScaleAlgorithm? and typeof src == 'string' # cache scaled images global.InventoryWindow_cachedScaledImages ?= {} if global.InventoryWindow_cachedScaledImages[src] setImage global.InventoryWindow_cachedScaledImages[src] else # generate scaled image, requires async callback img = new Image() img.onload = () => scaled = touchup.scale(img, @textureScale, @textureScale, @textureScaleAlgorithm) global.InventoryWindow_cachedScaledImages[src] = scaled setImage scaled img.src = src else # unscaled image setImage src # 3D cube node (for blocks) cubeNode = div.children[0] if not cubeNode? cubeNode = document.createElement('div') cubeNode.setAttribute 'style', 'position: relative; z-index: 0;' div.appendChild cubeNode cubeNode.removeChild(cubeNode.firstChild) while cubeNode.firstChild if Array.isArray(src) || typeof(src) == 'object' # 3d cube cube = new CubeIcon(images:src) cubeNode.appendChild cube.container # textual count textBox = div.children[1] if not 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 progressNode = div.children[2] if not progressNode? progressNode = document.createElement('div') progressNode.setAttribute 'style', " width: 0%; top: #{@textureSize - @borderSize * 2}px; position: relative; visibility: hidden; " div.appendChild progressNode progressNode.style.borderTop = "#{@progressThickness}px solid #{progressColor}" if progressColor? progressNode.style.width = (progress * 100) + '%' if progress? progressNode.style.visibility = if progress? then '' else 'hidden' # tooltip if @tooltips tooltipNode = div.children[3] if not tooltipNode? tooltipNode = document.createTextNode('not set') tooltip = createTooltip div, tooltipNode div.appendChild tooltip.div if itemPile? if @registry? tooltipText = @registry.getItemDisplayName itemPile.item else if @getTooltip? tooltipText = @getTooltip itemPile else tooltipText = '' tooltipNode.textContent = tooltipText getProgressBarColor: (progress) -> for threshold, i in @progressColorsThresholds if progress <= threshold return @progressColors[i] return @progressColors.slice(-1)[0] # default to last setBorderStyle: (node, index) -> x = index %% @width y = index // @width height = @inventorySize / @width if index == @selectedIndex kind = 'dotted' else kind = 'solid' node.style.border = "#{@borderSize}px #{kind} black" node.style.borderTop = "#{@borderSize * 2}px #{kind} black" if y == 0 node.style.borderBottom = "#{@borderSize * 2}px #{kind} black" if y == height - 1 node.style.borderLeft = "#{@borderSize * 2}px #{kind} black" if x == 0 node.style.borderRight = "#{@borderSize * 2}px #{kind} black" if x == @width - 1 setSelected: (index) -> @selectedIndex = index @refresh() # TODO: selective refresh? getSelected: (index) -> @selectedIndex refreshSlotNode: (index) -> @populateSlotNode @slotNodes[index], @inventory.get(index) @setBorderStyle @slotNodes[index], index refresh: () -> for i in [0...@inventorySize] @refreshSlotNode(i) positionAtMouse: (node, mouseEvent) -> x = mouseEvent.x ? mouseEvent.clientX y = mouseEvent.y ? mouseEvent.clientY x -= @textureSize / 2 y -= @textureSize / 2 node.style.left = x + 'px' node.style.top = y + 'px' createHeldNode: (itemPile, ev) -> @removeHeldNode() if global.InventoryWindow_heldNode if !itemPile or itemPile.count == 0 global.InventoryWindow_heldItemPile = undefined return global.InventoryWindow_heldItemPile = itemPile global.InventoryWindow_heldNode = @createSlotNode(global.InventoryWindow_heldItemPile) global.InventoryWindow_heldNode.setAttribute 'style', 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; " @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 @inventory.get(index) # drop one, but try to merge with existing oneHeld = global.InventoryWindow_heldItemPile.splitPile(1) if @inventory.get(index).mergePile(oneHeld) == false # could not merge, so swap global.InventoryWindow_heldItemPile.increase(1) tmp = global.InventoryWindow_heldItemPile global.InventoryWindow_heldItemPile = @inventory.get(index) @inventory.set(index, tmp) else @inventory.changed() else # drop on empty slot @inventory.set(index, global.InventoryWindow_heldItemPile.splitPile(1)) clickSlot: (index, ev) -> itemPile = @inventory.get(index) console.log 'clickSlot',index,itemPile global.InventoryWindow_mouseButtonDown = ev.button shiftDown = ev.shiftKey if ev.button != @secondaryMouseButton # left click: whole pile if not global.InventoryWindow_heldItemPile or not @allowDrop # pickup whole pile return if not @allowPickup if global.InventoryWindow_heldItemPile? # tried to drop on pickup-only inventory, so merge into held inventory instead if @inventory.get(index)? return if not global.InventoryWindow_heldItemPile.canPileWith @inventory.get(index) global.InventoryWindow_heldItemPile.mergePile @inventory.get(index) else if not shiftDown # simply picking up the whole pile global.InventoryWindow_heldItemPile = @inventory.get(index) @inventory.set(index, undefined) else if @linkedInventory and @inventory.get(index)? # shift-click: transfer to linked inventory @linkedInventory.give @inventory.get(index) @inventory.set index, undefined if @inventory.get(index).count == 0 @inventory.changed() # update source, might not have transferred all of the pile @emit 'pickup' # TODO: event data? index, item? cancelable? else # drop whole pile if @inventory.get(index) # try to merge piles dropped on each other if @inventory.get(index).mergePile(global.InventoryWindow_heldItemPile) == false # cannot pile together; swap dropped/held tmp = global.InventoryWindow_heldItemPile global.InventoryWindow_heldItemPile = @inventory.get(index) @inventory.set(index, tmp) else @inventory.changed() else # fill entire slot @inventory.set(index, global.InventoryWindow_heldItemPile) global.InventoryWindow_heldItemPile = undefined else # right-click: half/one if not global.InventoryWindow_heldItemPile # pickup half return if not @allowPickup global.InventoryWindow_heldItemPile = @inventory.get(index)?.splitPile(0.5) @inventory.set index, undefined if @inventory.get(index)?.count == 0 @inventory.changed() @emit 'pickup' # TODO: event data? index, item? cancelable? else return if not @allowDrop @dropOneHeld(index) @createHeldNode global.InventoryWindow_heldItemPile, ev @refreshSlotNode index