Added a swap button, as well as moving my testing to the demo view and adding some specifications, fixing bugs and tweaking interactions along the way.

This commit is contained in:
Scott Erickson 2014-08-14 13:41:32 -07:00
parent 719e64e85f
commit 827f6ee8b8
7 changed files with 204 additions and 66 deletions

View file

@ -15,7 +15,6 @@ module.exports = class CocoRouter extends Backbone.Router
routes:
'': go('HomeView')
'items': go('game-menu/InventoryView')
'about': go('AboutView')

View file

@ -1,17 +1,19 @@
$selected-area-height: 150px
@import "../bootstrap/mixins"
//#inventory-view *
#inventory-view
position: relative
height: 600px
background-color: white
user-select: none
+user-select(none)
h3
margin: 0
#equipped
padding: 10px
width: 74%
width: 75%
position: absolute
left: 0
top: 0
@ -24,7 +26,7 @@ $selected-area-height: 150px
.panel
text-align: center
width: 31%
width: 31.3%
margin: 5px 2% 5px 0
float: left
cursor: pointer
@ -42,29 +44,23 @@ $selected-area-height: 150px
font-size: 12px
#available-equipment
width: 24%
padding: 10px
width: 25%
position: absolute
right: 1%
right: 0
top: 0
bottom: $selected-area-height + 10
overflow: scroll
user-select: none
h3
margin-bottom: 5px
.list-group-item.active
background-color: #e0f0f5
.list-group-item.equipped
display: none
.item-mixin
&.active
background-color: lightblue
.well
padding: 5px
.item-view
cursor: pointer
#selected-items
@ -73,13 +69,17 @@ $selected-area-height: 150px
left: 0
right: 0
bottom: 0
text-align: center
#selected-equipped-item, #selected-available-item
text-align: left
position: absolute
bottom: 0
top: 0
overflow: scroll
padding: 10px
height: 100%
width: 49%
img
width: 100px
@ -90,8 +90,16 @@ $selected-area-height: 150px
#selected-equipped-item
left: 0
width: 48.5%
#selected-available-item
right: 2%
width: 48.5%
right: 0
$swap-item-height: 40px
#swap-button
position: relative
top: ($selected-area-height / 2) - ($swap-item-height / 2)
height: $swap-item-height
font-size: 24px
line-height: 24px
display: inline-block

View file

@ -16,7 +16,7 @@ div#equipped
- }
div#available-equipment
h3 Available
h3 Stash
for item in items
.list-group-item(class=item.classes, data-item-id=item.id)
@ -25,3 +25,5 @@ div#selected-items
.item-view-stub
#selected-available-item.well
.item-view-stub
button#swap-button.btn.btn-danger
span.glyphicon.glyphicon-transfer

View file

@ -87,6 +87,7 @@ module.exports = DemoView = class DemoView extends RootView
else
@$el.find('#demo-area').empty().append(view.$el)
view.afterInsert()
window.currentDemoView = view
# TODO, maybe handle root views differently than modal views differently than everything else?
getAllDemoFiles: ->

View file

@ -5,13 +5,6 @@ ThangType = require 'models/ThangType'
CocoCollection = require 'collections/CocoCollection'
ItemView = require './ItemView'
DEFAULT_EQUIPMENT = {
'right-hand': '53e21249b82921000051ce11'
'feet':'53e214f153457600003e3eab'
'eyes': '53e2167653457600003e3eb3'
'left-hand': '53e22aa153457600003e3ef5'
}
module.exports = class InventoryView extends CocoView
id: 'inventory-view'
className: 'tab-pane'
@ -23,6 +16,7 @@ module.exports = class InventoryView extends CocoView
'click #available-equipment .list-group-item': 'onAvailableItemClick'
'dblclick #available-equipment .list-group-item': 'onAvailableItemDoubleClick'
'dblclick .item-slot .item-view': 'onEquippedItemDoubleClick'
'click #swap-button': 'onClickSwapButton'
shortcuts:
'esc': 'clearSelection'
@ -30,7 +24,7 @@ module.exports = class InventoryView extends CocoView
initialize: (options) ->
super(arguments...)
@items = new CocoCollection([], { model: ThangType })
@equipment = options.equipment or DEFAULT_EQUIPMENT
@equipment = options.equipment or {}
@items.url = '/db/thang.type?view=items&project=name,description,components,original'
@supermodel.loadCollection(@items, 'items')
@ -87,28 +81,64 @@ module.exports = class InventoryView extends CocoView
onItemSlotClick: (e) ->
slot = $(e.target).closest('.panel')
wasActive = slot.hasClass('panel-info')
@$el.find('#equipped .panel').removeClass('panel-info')
@$el.find('#available-equipment .list-group-item').removeClass('active') if slot.hasClass('disabled')
slot.addClass('panel-info') # unless wasActive
@unselectAllSlots()
@unselectAllAvailableEquipment() if slot.hasClass('disabled')
@selectSlot(slot) unless wasActive and not $(e.target).closest('.item-view')[0]
@onSelectionChanged()
onAvailableItemClick: (e) ->
itemEl = $(e.target).closest('.list-group-item')
@$el.find('#available-equipment .list-group-item').removeClass('active')
itemEl.addClass('active')
itemContainer = $(e.target).closest('.list-group-item')
@unselectAllAvailableEquipment()
@selectAvailableItem(itemContainer)
@onSelectionChanged()
onAvailableItemDoubleClick: ->
slot = @$el.find('#equipped .item-slot.panel-info')
slot = $('.panel:not(.disabled):first') if not slot.length
onAvailableItemDoubleClick: (e) ->
slot = @getSelectedSlot()
slot = @$el.find('.panel:not(.disabled):first') if not slot.length
@unequipItemFromSlot(slot)
@equipSelectedItemToSlot(slot)
@onSelectionChanged()
onEquippedItemDoubleClick: (e) ->
@unselectAllAvailableEquipment()
slot = $(e.target).closest('.item-slot')
@unequipItemFromSlot(slot)
@selectAvailableItem(@unequipItemFromSlot(slot))
@onSelectionChanged()
onClickSwapButton: ->
slot = @getSelectedSlot()
selectedItemContainer = @$el.find('#available-equipment .list-group-item.active')
return unless slot[0] or selectedItemContainer[0]
slot = @$el.find('.panel:not(.disabled):first') if not slot.length
itemContainer = @unequipItemFromSlot(slot)
@equipSelectedItemToSlot(slot)
@selectAvailableItem(itemContainer)
@selectSlot(slot)
@onSelectionChanged()
getSelectedSlot: ->
@$el.find('#equipped .item-slot.panel-info')
unselectAllAvailableEquipment: ->
@$el.find('#available-equipment .list-group-item').removeClass('active')
unselectAllSlots: ->
@$el.find('#equipped .panel').removeClass('panel-info')
selectSlot: (slot) ->
slot.addClass('panel-info')
getSlot: (name) ->
@$el.find(".item-slot[data-slot=#{name}]")
getSelectedAvailableItemContainer: ->
@$el.find('#available-equipment .list-group-item.active')
getAvailableItemContainer: (itemID) ->
@$el.find("#available-equipment .list-group-item[data-item-id='#{itemID}']")
selectAvailableItem: (itemContainer) ->
itemContainer?.addClass('active')
unequipItemFromSlot: (slot) ->
itemIDToUnequip = slot.find('.item-view').data('item-id')
@ -117,15 +147,15 @@ module.exports = class InventoryView extends CocoView
for el in @$el.find('#available-equipment .list-group-item')
itemID = $(el).find('.item-view').data('item-id')
if itemID is itemIDToUnequip
$(el).removeClass('equipped')
return $(el).removeClass('equipped')
equipSelectedItemToSlot: (slot) ->
selectedItemContainer = @$el.find('#available-equipment .list-group-item.active')
selectedItemContainer = @getSelectedAvailableItemContainer()
newItemHTML = selectedItemContainer.html()
@$el.find('#available-equipment .list-group-item.active').addClass('equipped')
container = slot.find('.panel-body')
container.html(newItemHTML)
container.find('.item-view').data('item-id', selectedItemContainer.find('.item-view').data('item-id'))
selectedItemContainer .addClass('equipped')
slotContainer = slot.find('.panel-body')
slotContainer.html(newItemHTML)
slotContainer.find('.item-view').data('item-id', selectedItemContainer.find('.item-view').data('item-id'))
@$el.find('.list-group-item').removeClass('active')
onSelectionChanged: ->
@ -141,19 +171,10 @@ module.exports = class InventoryView extends CocoView
selectedSlotItemID = selectedSlot.find('.item-view').data('item-id')
if selectedSlotItemID
item = _.find @items.models, {id:selectedSlotItemID}
if not @selectedEquippedItemView
@selectedEquippedItemView = new ItemView({
item: item, includes: {name: true, stats: true}})
@insertSubView(@selectedEquippedItemView, @$el.find('#selected-equipped-item .item-view-stub'))
else
@selectedEquippedItemView.$el.show()
@selectedEquippedItemView.item = item
@selectedEquippedItemView.render()
@showSelectedSlotItem(item)
else
@selectedEquippedItemView?.$el.hide()
@hideSelectedSlotItem()
else
@$el.find('#available-equipment .list-group-item').show()
@ -170,21 +191,40 @@ module.exports = class InventoryView extends CocoView
if slotName not in allowedSlots
$(slotEl).addClass('disabled')
# updated selected item view
if not @selectedAvailableItemView
@selectedAvailableItemView = new ItemView({
item: item, includes: {name: true, stats: true}})
@insertSubView(@selectedAvailableItemView, @$el.find('#selected-available-item .item-view-stub'))
else
@selectedAvailableItemView.$el.show()
@selectedAvailableItemView.item = item
@selectedAvailableItemView.render()
@showSelectedAvailableItem(item)
else
@selectedAvailableItemView?.$el.hide()
@hideSelectedAvailableItem()
@delegateEvents()
showSelectedSlotItem: (item) ->
if not @selectedEquippedItemView
@selectedEquippedItemView = new ItemView({
item: item, includes: {name: true, stats: true}})
@insertSubView(@selectedEquippedItemView, @$el.find('#selected-equipped-item .item-view-stub'))
else
@selectedEquippedItemView.$el.show()
@selectedEquippedItemView.item = item
@selectedEquippedItemView.render()
hideSelectedSlotItem: ->
@selectedEquippedItemView?.$el.hide()
showSelectedAvailableItem: (item) ->
if not @selectedAvailableItemView
@selectedAvailableItemView = new ItemView({
item: item, includes: {name: true, stats: true}})
@insertSubView(@selectedAvailableItemView, @$el.find('#selected-available-item .item-view-stub'))
else
@selectedAvailableItemView.$el.show()
@selectedAvailableItemView.item = item
@selectedAvailableItemView.render()
hideSelectedAvailableItem: ->
@selectedAvailableItemView?.$el.hide()
getCurrentEquipmentConfig: ->
config = {}

View file

@ -0,0 +1,71 @@
InventoryView = require 'views/game-menu/InventoryView'
thangTypes = [
{"_id":"boots-id","name":"Boots","original":"boots","components":[{"original":"524b85837fc0f6d519000020","majorVersion":0},{"original":"524b7b857fc0f6d519000012","majorVersion":0},{"original":"524b4150ff92f1f4f8000024","majorVersion":0},{"original":"53e12043b82921000051cdf9","majorVersion":0,"config":{"slots":["feet"],"programmableProperties":["move","targetPos"],"moreProgrammableProperties":[],"extraHUDProperties":["maxSpeed"],"stats":{"maxSpeed":{"factor":1}}}},{"original":"524b7b8c7fc0f6d519000013","majorVersion":0,"config":{"locomotionType":"running","maxSpeed":5,"maxAcceleration":100}},{"original":"524b75ad7fc0f6d519000001","majorVersion":0,"config":{"pos":{"x":39.08,"y":20.72,"z":0.5},"width":1,"height":1,"depth":1,"shape":"ellipsoid"}},{"original":"524b7b7c7fc0f6d519000011","majorVersion":0}]},
{"_id":"boots-of-leaping-id","name":"Boots of Leaping","original":"boots-of-leaping","components":[{"original":"524b85837fc0f6d519000020","majorVersion":0},{"original":"524b7b857fc0f6d519000012","majorVersion":0},{"original":"524b4150ff92f1f4f8000024","majorVersion":0},{"original":"53e12043b82921000051cdf9","majorVersion":0,"config":{"ownerID":"Tharin","slots":["feet"],"programmableProperties":["move","targetPos","jumpTo"],"moreProgrammableProperties":["jump"],"extraHUDProperties":["maxSpeed"],"stats":{"maxSpeed":{"factor":1.2}}}},{"original":"524b7b8c7fc0f6d519000013","majorVersion":0,"config":{"locomotionType":"running","maxSpeed":6,"maxAcceleration":100}},{"original":"524b1f54d768d916b5000001","majorVersion":0,"config":{"jumpHeight":3}},{"original":"5275392d69abdcb12401441e","majorVersion":0,"config":{"jumpSpeedFactor":1.5}},{"original":"524b75ad7fc0f6d519000001","majorVersion":0,"config":{"pos":{"x":39.08,"y":20.72,"z":0.5},"width":1,"height":1,"depth":1,"shape":"ellipsoid"}},{"original":"524b7b7c7fc0f6d519000011","majorVersion":0}]},
{"_id":"crossbow-id","name":"Crossbow","original":"crossbow","components":[{"original":"524b85837fc0f6d519000020","majorVersion":0},{"original":"524b517fff92f1f4f8000046","majorVersion":0},{"original":"524b7b747fc0f6d519000010","majorVersion":0,"config":{"team":"humans"}},{"original":"524b7bc67fc0f6d51900001a","majorVersion":0,"config":{"missileThangID":"Arrow"}},{"original":"524b7ba57fc0f6d519000016","majorVersion":0,"config":{"attackDamage":5,"attackRange":20,"cooldown":0.6,"chasesWhenAttackingOutOfRange":true}},{"original":"524b3e3fff92f1f4f800000d","majorVersion":0},{"original":"524cbdc03ea855e0ab0000bb","majorVersion":0},{"original":"524b4150ff92f1f4f8000024","majorVersion":0},{"original":"53e12043b82921000051cdf9","majorVersion":0,"config":{"slots":["right-hand"],"programmableProperties":["attack","target","attackRange"],"moreProgrammableProperties":["attackXY","targetPos"],"extraHUDProperties":["attackDamage","attackRange"]}},{"original":"524b75ad7fc0f6d519000001","majorVersion":0,"config":{"pos":{"x":41.105000000000004,"y":31.6,"z":0.125},"width":1.5,"height":0.75,"depth":0.25,"shape":"box"}},{"original":"524b7b7c7fc0f6d519000011","majorVersion":0},{"original":"524b457bff92f1f4f8000031","majorVersion":0}]},
{"_id":"crude-glasses-id","name":"Crude Glasses","original":"crude-glasses","components":[{"original":"524b7b747fc0f6d519000010","majorVersion":0,"config":{"team":"humans"}},{"original":"524b4150ff92f1f4f8000024","majorVersion":0},{"original":"53e12043b82921000051cdf9","majorVersion":0,"config":{"slots":["eyes"],"programmableProperties":["pos","getEnemies"],"moreProgrammableProperties":["getItems","getFriends"]}},{"original":"524b75ad7fc0f6d519000001","majorVersion":0,"config":{"pos":{"x":33.230000000000004,"y":20.75,"z":2},"width":1,"height":2,"depth":1,"shape":"ellipsoid"}},{"original":"524b457bff92f1f4f8000031","majorVersion":0,"config":{"visualRange":50}}]}
]
describe 'InventoryView', ->
inventoryView = null
beforeEach (done) ->
equipment = { 'feet':'boots', 'eyes': 'crude-glasses' }
inventoryView = new InventoryView({ equipment: equipment })
responses =
'/db/thang.type?view=items': thangTypes
jasmine.Ajax.requests.sendResponses(responses)
_.defer ->
inventoryView.render()
done()
it 'selects a slot when you click it', ->
inventoryView.getSlot('eyes').click()
expect(inventoryView.getSelectedSlot().data('slot')).toBe('eyes')
it 'unselects a selected slot when you click it', ->
inventoryView.getSlot('eyes').click().click()
expect(inventoryView.getSelectedSlot().data('slot')).toBeUndefined()
it 'selects an available item when you click it', ->
inventoryView.getAvailableItemContainer('boots-of-leaping-id').click()
expect(inventoryView.getSelectedAvailableItemContainer().data('item-id')).toBe('boots-of-leaping-id')
it 'equips an available item when you double click it', ->
inventoryView.getAvailableItemContainer('crossbow-id').click().dblclick()
expect(inventoryView.getCurrentEquipmentConfig()['right-hand']).toBeTruthy()
it 'unequips an itm when you double click it', ->
inventoryView.getSlot('eyes').find('.item-view').click().dblclick()
expect(inventoryView.getCurrentEquipmentConfig().eyes).toBeUndefined()
describe 'swap button', ->
it 'does nothing if nothing is selected', ->
inventoryView.$el.find('#swap-button').click()
expect(inventoryView.getSelectedSlot()[0]).toBeFalsy()
expect(inventoryView.getSelectedAvailableItemContainer()[0]).toBeFalsy()
it 'unequips and selects the unequipped item if just an equipped slot is chosen', ->
expect(inventoryView.getCurrentEquipmentConfig().eyes).toBeTruthy()
slot = inventoryView.getSlot('eyes')
inventoryView.selectSlot(slot)
inventoryView.$el.find('#swap-button').click()
expect(inventoryView.getCurrentEquipmentConfig().eyes).toBeUndefined()
expect(inventoryView.getSelectedAvailableItemContainer().data('item-id')).toBe('crude-glasses-id')
it 'equips the selected item if just an available item is selected', ->
expect(inventoryView.getCurrentEquipmentConfig()['right-hand']).toBeUndefined()
inventoryView.getAvailableItemContainer('crossbow-id').click()
inventoryView.$el.find('#swap-button').click()
expect(inventoryView.getCurrentEquipmentConfig()['right-hand']).toBeTruthy()
expect(inventoryView.getSelectedAvailableItemContainer().data('item-id')).toBeUndefined()
expect(inventoryView.getSelectedSlot().data('slot')).toBe('right-hand')
it 'swaps items if both a slot and item are selected, and keeps them selected', ->
inventoryView.getAvailableItemContainer('boots-of-leaping-id').click()
inventoryView.getSlot('feet').click()
inventoryView.$el.find('#swap-button').click()
expect(inventoryView.getCurrentEquipmentConfig()['feet']).toBe('boots-of-leaping')
expect(inventoryView.getSelectedAvailableItemContainer().data('item-id')).toBe('boots-id')
expect(inventoryView.getSelectedSlot().data('slot')).toBe('feet')

View file

@ -0,0 +1,17 @@
InventoryView = require 'views/game-menu/InventoryView'
thangTypes = [
{"_id":"53eb98ff1a100989a40ce46b","name":"Boots","original":"boots","components":[{"original":"524b85837fc0f6d519000020","majorVersion":0},{"original":"524b7b857fc0f6d519000012","majorVersion":0},{"original":"524b4150ff92f1f4f8000024","majorVersion":0},{"original":"53e12043b82921000051cdf9","majorVersion":0,"config":{"slots":["feet"],"programmableProperties":["move","targetPos"],"moreProgrammableProperties":[],"extraHUDProperties":["maxSpeed"],"stats":{"maxSpeed":{"factor":1}}}},{"original":"524b7b8c7fc0f6d519000013","majorVersion":0,"config":{"locomotionType":"running","maxSpeed":5,"maxAcceleration":100}},{"original":"524b75ad7fc0f6d519000001","majorVersion":0,"config":{"pos":{"x":39.08,"y":20.72,"z":0.5},"width":1,"height":1,"depth":1,"shape":"ellipsoid"}},{"original":"524b7b7c7fc0f6d519000011","majorVersion":0}]},
{"_id":"53eb98851a100989a40ce460","name":"Boots of Leaping","original":"boots-of-leaping","components":[{"original":"524b85837fc0f6d519000020","majorVersion":0},{"original":"524b7b857fc0f6d519000012","majorVersion":0},{"original":"524b4150ff92f1f4f8000024","majorVersion":0},{"original":"53e12043b82921000051cdf9","majorVersion":0,"config":{"ownerID":"Tharin","slots":["feet"],"programmableProperties":["move","targetPos","jumpTo"],"moreProgrammableProperties":["jump"],"extraHUDProperties":["maxSpeed"],"stats":{"maxSpeed":{"factor":1.2}}}},{"original":"524b7b8c7fc0f6d519000013","majorVersion":0,"config":{"locomotionType":"running","maxSpeed":6,"maxAcceleration":100}},{"original":"524b1f54d768d916b5000001","majorVersion":0,"config":{"jumpHeight":3}},{"original":"5275392d69abdcb12401441e","majorVersion":0,"config":{"jumpSpeedFactor":1.5}},{"original":"524b75ad7fc0f6d519000001","majorVersion":0,"config":{"pos":{"x":39.08,"y":20.72,"z":0.5},"width":1,"height":1,"depth":1,"shape":"ellipsoid"}},{"original":"524b7b7c7fc0f6d519000011","majorVersion":0}]},
{"_id":"53e2288553457600003e3ee2","name":"Crossbow","original":"crossbow","components":[{"original":"524b85837fc0f6d519000020","majorVersion":0},{"original":"524b517fff92f1f4f8000046","majorVersion":0},{"original":"524b7b747fc0f6d519000010","majorVersion":0,"config":{"team":"humans"}},{"original":"524b7bc67fc0f6d51900001a","majorVersion":0,"config":{"missileThangID":"Arrow"}},{"original":"524b7ba57fc0f6d519000016","majorVersion":0,"config":{"attackDamage":5,"attackRange":20,"cooldown":0.6,"chasesWhenAttackingOutOfRange":true}},{"original":"524b3e3fff92f1f4f800000d","majorVersion":0},{"original":"524cbdc03ea855e0ab0000bb","majorVersion":0},{"original":"524b4150ff92f1f4f8000024","majorVersion":0},{"original":"53e12043b82921000051cdf9","majorVersion":0,"config":{"slots":["right-hand"],"programmableProperties":["attack","target","attackRange"],"moreProgrammableProperties":["attackXY","targetPos"],"extraHUDProperties":["attackDamage","attackRange"]}},{"original":"524b75ad7fc0f6d519000001","majorVersion":0,"config":{"pos":{"x":41.105000000000004,"y":31.6,"z":0.125},"width":1.5,"height":0.75,"depth":0.25,"shape":"box"}},{"original":"524b7b7c7fc0f6d519000011","majorVersion":0},{"original":"524b457bff92f1f4f8000031","majorVersion":0}]},
{"_id":"53ecec87415ce434054f6aac","name":"Crude Glasses","original":"crude-glasses","components":[{"original":"524b7b747fc0f6d519000010","majorVersion":0,"config":{"team":"humans"}},{"original":"524b4150ff92f1f4f8000024","majorVersion":0},{"original":"53e12043b82921000051cdf9","majorVersion":0,"config":{"slots":["eyes"],"programmableProperties":["pos","getEnemies"],"moreProgrammableProperties":["getItems","getFriends"]}},{"original":"524b75ad7fc0f6d519000001","majorVersion":0,"config":{"pos":{"x":33.230000000000004,"y":20.75,"z":2},"width":1,"height":2,"depth":1,"shape":"ellipsoid"}},{"original":"524b457bff92f1f4f8000031","majorVersion":0,"config":{"visualRange":50}}]}
]
module.exports = ->
equipment = { 'feet':'boots', 'eyes': 'crude-glasses' }
view = new InventoryView({ equipment: equipment})
view.render()
responses =
'/db/thang.type?view=items': thangTypes
jasmine.Ajax.requests.sendResponses(responses)
return view