hackedvoxels-inventory-window
Version:
a GUI window for interactively manipulating an inventory of itempiles
399 lines (336 loc) • 14.1 kB
text/coffeescript
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