ModalView = require 'views/kinds/ModalView' template = require 'templates/game-menu/inventory-modal' {me} = require 'lib/auth' ThangType = require 'models/ThangType' CocoCollection = require 'collections/CocoCollection' ItemView = require './ItemView' SpriteBuilder = require 'lib/sprites/SpriteBuilder' hasGoneFullScreenOnce = false module.exports = class InventoryModal extends ModalView id: 'inventory-modal' className: 'modal fade play-modal' template: template slots: ['head', 'eyes', 'neck', 'torso', 'wrists', 'gloves', 'left-ring', 'right-ring', 'right-hand', 'left-hand', 'waist', 'feet', 'programming-book', 'pet', 'minion', 'flag'] #, 'misc-0', 'misc-1'] # TODO: bring in misc slot(s) again when we have space events: 'click .item-slot': 'onItemSlotClick' 'click #available-equipment .list-group-item:not(.equipped)': 'onAvailableItemClick' 'dblclick #available-equipment .list-group-item:not(.equipped)': 'onAvailableItemDoubleClick' 'doubletap #available-equipment .list-group-item:not(.equipped)': 'onAvailableItemDoubleClick' 'dblclick .item-slot .item-view': 'onEquippedItemDoubleClick' 'doubletap .item-slot .item-view': 'onEquippedItemDoubleClick' 'shown.bs.modal': 'onShown' 'click #choose-hero-button': 'onClickChooseHero' 'click #play-level-button': 'onClickPlayLevel' shortcuts: 'esc': 'clearSelection' 'enter': 'onClickPlayLevel' initialize: (options) -> super(arguments...) @items = new CocoCollection([], {model: ThangType}) @equipment = options.equipment or @options.session?.get('heroConfig')?.inventory or me.get('heroConfig')?.inventory or {} @equipment = $.extend true, {}, @equipment @requireLevelEquipment() @items.url = '/db/thang.type?view=items&project=name,slug,components,original,rasterIcon,gems,description,heroClass' @supermodel.loadCollection(@items, 'items') destroy: -> @stage?.removeAllChildren() super() onLoaded: -> item.notInLevel = true for item in @items.models super() getRenderData: (context={}) -> context = super(context) context.equipped = _.values(@equipment) context.items = @items.models for item in @items.models item.classes = item.getAllowedSlots() item.classes.push 'equipped' if item.get('original') in context.equipped locked = @allowedItems and not (item.get('original') in @allowedItems) item.classes.push 'locked' if locked and item.get('slug') isnt 'simple-boots' for heroClass in item.getAllowedHeroClasses() item.classes.push heroClass item.classes.push 'silhouette' if item.isSilhouettedItem() item.classes.push 'restricted' if item.get('slug') in _.values(restrictedGearByLevel[@options.levelID] ? {}) @items.models.sort (a, b) -> lockScore = 90019001 * (('locked' in a.classes) - ('locked' in b.classes)) gemScore = a.get('gems') - b.get('gems') lockScore + gemScore context.unlockedItems = [] context.lockedItems = [] for item in @items.models (if 'locked' in item.classes then context.lockedItems else context.unlockedItems).push item context.slots = @slots context.equipment = _.clone @equipment for slot, itemOriginal of context.equipment item = _.find @items.models, (item) -> item.get('original') is itemOriginal context.equipment[slot] = item context afterRender: -> super() return unless @supermodel.finished() keys = (item.get('original') for item in @items.models) itemMap = _.zipObject keys, @items.models # Fill in equipped items for slottedItemStub in @$el.find('.replace-me') itemID = $(slottedItemStub).data('item-id') item = itemMap[itemID] itemView = new ItemView({item: item, includes: {}}) itemView.render() $(slottedItemStub).replaceWith(itemView.$el) @registerSubView(itemView) for availableItemEl in @$el.find('#available-equipment .list-group-item') itemID = $(availableItemEl).data('item-id') item = itemMap[itemID] itemView = new ItemView({item: item, includes: {name: true}}) itemView.render() $(availableItemEl).append(itemView.$el) @registerSubView(itemView) continue if $(availableItemEl).hasClass('locked') or $(availableItemEl).hasClass('restricted') dragHelper = itemView.$el.find('img').clone().addClass('draggable-item') do (dragHelper, itemView) => itemView.$el.draggable revert: 'invalid' appendTo: @$el cursorAt: {left: 35.5, top: 35.5} helper: -> dragHelper revertDuration: 200 distance: 10 scroll: false zIndex: 100 itemView.$el.on 'dragstart', => @onAvailableItemClick target: itemView.$el.parent() unless itemView.$el.parent().hasClass 'active' for itemSlot in @$el.find '.item-slot' slot = $(itemSlot).data 'slot' do (slot, itemSlot) => $(itemSlot).droppable drop: (e, ui) => @onAvailableItemDoubleClick() accept: (el) -> $(el).parent().hasClass slot activeClass: 'droppable' hoverClass: 'droppable-hover' tolerance: 'touch' @makeEquippedSlotDraggable $(itemSlot) @$el.find('.hero-container').droppable drop: (e, ui) => @onAvailableItemDoubleClick() accept: (el) -> true activeClass: 'droppable' hoverClass: 'droppable-hover' tolerance: 'pointer' @$el.find('#selected-items').hide() # Hide until one is selected @delegateEvents() if @selectedHero and not @startedLoadingFirstHero @loadHero() @requireLevelEquipment() afterInsert: -> super() @canvasWidth = @$el.find('canvas').innerWidth() @canvasHeight = @$el.find('canvas').innerHeight() @inserted = true makeEquippedSlotDraggable: (slot) -> unequip = => @unequipItemFromSlot slot shouldStayEquippedWhenDropped = (isValidDrop) -> pos = $(@).position() revert = Math.abs(pos.left) < $(@).outerWidth() and Math.abs(pos.top) < $(@).outerHeight() unequip() if not revert revert # TODO: figure out how to make this actually above the available items list (the .ui-draggable-helper img is still inside .item-view and so underlaps...) $(slot).find('img').draggable revert: shouldStayEquippedWhenDropped appendTo: @$el cursorAt: {left: 35.5, top: 35.5} revertDuration: 200 distance: 10 scroll: false zIndex: 100 clearSelection: -> @$el.find('.item-slot.selected').removeClass 'selected' @$el.find('.list-group-item').removeClass('active') @onSelectionChanged() onItemSlotClick: (e) -> return if @remainingRequiredEquipment?.length # Don't let them select a slot if we need them to first equip some require gear. slot = $(e.target).closest('.item-slot') wasActive = slot.hasClass('selected') @unselectAllSlots() @unselectAllAvailableEquipment() if slot.hasClass('disabled') if wasActive @hideSelectedSlotItem() @unselectAllAvailableEquipment() else @selectSlot(slot) @onSelectionChanged() onAvailableItemClick: (e) -> itemContainer = $(e.target).closest('.list-group-item') return if itemContainer.hasClass('locked') or itemContainer.hasClass('restricted') wasActive = itemContainer.hasClass 'active' @unselectAllAvailableEquipment() @selectAvailableItem(itemContainer) unless wasActive @onSelectionChanged() onAvailableItemDoubleClick: (e) -> if e itemContainer = $(e.target).closest('.list-group-item') return if itemContainer.hasClass('locked') or itemContainer.hasClass('restricted') @selectAvailableItem itemContainer @onSelectionChanged() slot = @getSelectedSlot() slot = @$el.find('.item-slot:not(.disabled):first') if not slot.length $(e.target).effect('transfer', to: slot, duration: 500, easing: 'easeOutCubic') if e @unequipItemFromSlot(slot) @equipSelectedItemToSlot(slot) @onSelectionChanged() onEquippedItemDoubleClick: (e) -> @unselectAllAvailableEquipment() slot = $(e.target).closest('.item-slot') @selectAvailableItem(@unequipItemFromSlot(slot)) @onSelectionChanged() getSelectedSlot: -> @$el.find('#equipped .item-slot.selected') unselectAllAvailableEquipment: -> @$el.find('#available-equipment .list-group-item').removeClass('active') unselectAllSlots: -> @$el.find('#equipped .item-slot.selected').removeClass('selected') selectSlot: (slot) -> slot.addClass('selected') 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') return unless itemIDToUnequip slot.find('.item-view').detach() for el in @$el.find('#available-equipment .list-group-item') itemID = $(el).find('.item-view').data('item-id') if itemID is itemIDToUnequip unequipped = $(el).removeClass('equipped') break if unequipped @clearSelection() @requireLevelEquipment() return unequipped equipSelectedItemToSlot: (slot) -> selectedItemContainer = @getSelectedAvailableItemContainer() newItemHTML = selectedItemContainer.html() selectedItemContainer.addClass('equipped') slotContainer = slot.find('.item-container') slotContainer.html(newItemHTML) slotContainer.find('.item-view').data('item-id', selectedItemContainer.find('.item-view').data('item-id')) @$el.find('.list-group-item').removeClass('active') @makeEquippedSlotDraggable slot @requireLevelEquipment() onSelectionChanged: -> @$el.find('.item-slot').show() selectedSlot = @$el.find('.item-slot.selected') selectedItem = @$el.find('#available-equipment .list-group-item.active') if selectedSlot.length @$el.find('#available-equipment .list-group-item').hide() unlockedCount = @$el.find("#available-equipment .list-group-item.#{selectedSlot.data('slot')}:not(.locked)").show().length lockedCount = @$el.find("#available-equipment .list-group-item.#{selectedSlot.data('slot')}.locked").show().length @$el.find('#unlocked-description').text("#{unlockedCount} #{selectedSlot.data('slot')} items owned").toggle unlockedCount > 0 @$el.find('#locked-description').text("#{lockedCount} #{selectedSlot.data('slot')} items locked").toggle lockedCount > 0 selectedSlotItemID = selectedSlot.find('.item-view').data('item-id') if selectedSlotItemID item = _.find @items.models, {id: selectedSlotItemID} @showSelectedSlotItem(item) else @hideSelectedSlotItem() else unlockedCount = @$el.find('#available-equipment .list-group-item:not(.locked)').show().length @$el.find('#available-equipment .list-group-item.locked').hide() @$el.find('#unlocked-description').text("#{unlockedCount} items owned").toggle unlockedCount > 0 @$el.find('#locked-description').text("#{lockedCount} items locked").hide() #@$el.find('#available-equipment .list-group-item.equipped').hide() @$el.find('.item-slot').removeClass('disabled') if selectedItem.length item = _.find @items.models, {id:selectedItem.find('.item-view').data('item-id')} # update which slots are enabled allowedSlots = item.getAllowedSlots() for slotEl in @$el.find('.item-slot') slotName = $(slotEl).data('slot') if slotName not in allowedSlots $(slotEl).addClass('disabled') @showSelectedAvailableItem(item) else @hideSelectedAvailableItem() @delegateEvents() showSelectedSlotItem: (item) -> if not @selectedEquippedItemView @selectedEquippedItemView = new ItemView({ item: item, includes: {name: true, stats: true, props: true}}) @insertSubView(@selectedEquippedItemView, @$el.find('#selected-equipped-item .item-view-stub')) else @selectedEquippedItemView.$el.show() @selectedEquippedItemView.item = item @selectedEquippedItemView.render() @$el.find('#selected-items').show() @$el.find('#selected-equipped-item').show() hideSelectedSlotItem: -> @selectedEquippedItemView?.$el.hide().parent().hide() @$el.find('#selected-items').hide() unless @selectedEquippedItemView?.$el?.is(':visible') showSelectedAvailableItem: (item) -> if not @selectedAvailableItemView @selectedAvailableItemView = new ItemView({ item: item, includes: {name: true, stats: true, props: true}}) @insertSubView(@selectedAvailableItemView, @$el.find('#selected-available-item .item-view-stub')) else @selectedAvailableItemView.$el.show() @selectedAvailableItemView.item = item @selectedAvailableItemView.render() @$el.find('#selected-items').show() @$el.find('#selected-available-item').show() hideSelectedAvailableItem: -> @selectedAvailableItemView?.$el.hide().parent().hide() @$el.find('#selected-items').hide() unless @selectedEquippedItemView?.$el?.is(':visible') getCurrentEquipmentConfig: -> config = {} for slot in @$el.find('.item-slot') slotName = $(slot).data('slot') slotItemID = $(slot).find('.item-view').data('item-id') continue unless slotItemID item = _.find @items.models, {id:slotItemID} config[slotName] = item.get('original') config requireLevelEquipment: -> # This is temporary, until we have a more general way of awarding items and configuring required/restricted items per level. return unless necessaryGear = requiredGearByLevel[@options.levelID] restrictedGear = restrictedGearByLevel[@options.levelID] ? {} if @inserted if @supermodel.finished() equipment = @getCurrentEquipmentConfig() # Make sure @equipment is updated else equipment = @equipment hadRequired = @remainingRequiredEquipment?.length @remainingRequiredEquipment = [] @$el.find('.should-equip').removeClass('should-equip') inWorldMap = $('#world-map-view').length for slot, item of restrictedGear equipped = equipment[slot] if equipped and equipped is gear[restrictedGear[slot]] console.log 'Unequipping restricted item', restrictedGear[slot], 'for', slot, 'before level', @options.levelID @unequipItemFromSlot @$el.find(".item-slot[data-slot='#{slot}']") for slot, item of necessaryGear continue if item is 'leather-tunic' and inWorldMap # Don't tell them they need it until they need it in the level equipped = equipment[slot] continue if equipped and not ((item is 'builders-hammer' and equipped is gear['simple-sword']) or (item is 'leather-boots' and equipped is gear['simple-boots'])) availableSlotSelector = "#available-equipment li[data-item-id='#{gear[item]}']" @highlightElement availableSlotSelector, delay: 500, sides: ['right'], rotation: Math.PI / 2 @$el.find(availableSlotSelector).addClass 'should-equip' @$el.find("#equipped div[data-slot='#{slot}']").addClass 'should-equip' @remainingRequiredEquipment.push slot: slot, item: gear[item] if hadRequired and not @remainingRequiredEquipment.length @endHighlight() @highlightElement (if inWorldMap then '#play-level-button' else '.overlaid-close-button'), duration: 5000 $('#play-level-button').prop('disabled', @remainingRequiredEquipment.length > 0) # Restrict available items to those that would be available by this item. @allowedItems = [] for level, items of requiredGearByLevel for slot, item of items @allowedItems.push gear[item] unless gear[item] in @allowedItems break if level is @options.levelID for item in me.items() when not (item in @allowedItems) @allowedItems.push item setHero: (@selectedHero) -> @loadHero() @$el.removeClass('Warrior Ranger Wizard').addClass(@selectedHero.get('heroClass')) loadHero: -> return unless @supermodel.finished() and @selectedHero and not @$el.hasClass 'secret' @startedLoadingFirstHero = true @stage?.removeAllChildren() if featureImage = @selectedHero.get 'featureImage' @$el.find(".equipped-hero-canvas").hide() @$el.find(".hero-feature-image").show().find('img').prop('src', '/file/' + featureImage) return if @selectedHero.loaded and movieClip = @movieClips?[@selectedHero.get('original')] @stage.addChild(movieClip) @stage.update() return onLoaded = => return unless canvas = @$el.find(".equipped-hero-canvas") @canvasWidth ||= canvas.width() @canvasHeight ||= canvas.height() canvas.prop width: @canvasWidth, height: @canvasHeight builder = new SpriteBuilder(@selectedHero) movieClip = builder.buildMovieClip(@selectedHero.get('actions').attack?.animation ? @selectedHero.get('actions').idle.animation) movieClip.scaleX = movieClip.scaleY = canvas.prop('height') / 120 # Average hero height is ~110px at normal resolution if @selectedHero.get('name') in ['Knight', 'Robot Walker'] # These are too big, so shrink them. movieClip.scaleX *= 0.7 movieClip.scaleY *= 0.7 movieClip.regX = -@selectedHero.get('positions').registration.x movieClip.regY = -@selectedHero.get('positions').registration.y movieClip.x = canvas.prop('width') * 0.5 movieClip.y = canvas.prop('height') * 0.95 # This is where the feet go. movieClip.gotoAndPlay 0 @stage ?= new createjs.Stage(canvas[0]) @stage.addChild movieClip @stage.update() @movieClips ?= {} @movieClips[@selectedHero.get('original')] = movieClip if @selectedHero.loaded if @selectedHero.isFullyLoaded() _.defer onLoaded else console.error 'Hmm, trying to render a hero we have not loaded...?', @selectedHero else @listenToOnce @selectedHero, 'sync', onLoaded onShown: -> # Called when we switch tabs to this within the modal @requireLevelEquipment() @loadHero() onHidden: -> # Called when the modal itself is dismissed @endHighlight() onClickChooseHero: -> @hide() @trigger 'choose-hero-click' onClickPlayLevel: (e) -> return if @$el.find('#play-level-button').prop 'disabled' @showLoading() ua = navigator.userAgent.toLowerCase() unless hasGoneFullScreenOnce or (/safari/.test(ua) and not /chrome/.test(ua)) or $(window).height() >= 658 # Min vertical resolution needed at 1366px wide @toggleFullscreen() hasGoneFullScreenOnce = true @updateConfig => @trigger 'play-click' window.tracker?.trackEvent 'Play Level Modal', Action: 'Play' updateConfig: (callback, skipSessionSave) -> sessionHeroConfig = @options.session.get('heroConfig') ? {} lastHeroConfig = me.get('heroConfig') ? {} inventory = @getCurrentEquipmentConfig() patchSession = patchMe = false patchSession ||= not _.isEqual inventory, sessionHeroConfig.inventory patchMe ||= not _.isEqual inventory, lastHeroConfig.inventory sessionHeroConfig.inventory = inventory lastHeroConfig.inventory = inventory if patchMe console.log 'setting me.heroConfig to', JSON.stringify(lastHeroConfig) me.set 'heroConfig', lastHeroConfig me.patch() if patchSession console.log 'setting session.heroConfig to', JSON.stringify(sessionHeroConfig) @options.session.set 'heroConfig', sessionHeroConfig @options.session.patch success: callback unless skipSessionSave else callback?() gear = 'simple-boots': '53e237bf53457600003e3f05' 'simple-sword': '53e218d853457600003e3ebe' 'leather-tunic': '53e22eac53457600003e3efc' 'leather-boots': '53e2384453457600003e3f07' 'leather-belt': '5437002a7beba4a82024a97d' 'programmaticon-i': '53e4108204c00d4607a89f78' 'crude-glasses': '53e238df53457600003e3f0b' 'builders-hammer': '53f4e6e3d822c23505b74f42' requiredGearByLevel = 'dungeons-of-kithgard': {feet: 'simple-boots'} 'gems-in-the-deep': {feet: 'simple-boots'} 'shadow-guard': {feet: 'simple-boots'} 'kounter-kithwise': {feet: 'simple-boots'} 'crawlways-of-kithgard': {feet: 'simple-boots'} 'forgetful-gemsmith': {feet: 'simple-boots'} 'true-names': {feet: 'simple-boots', 'right-hand': 'simple-sword', waist: 'leather-belt'} 'favorable-odds': {feet: 'simple-boots', 'right-hand': 'simple-sword'} 'the-raised-sword': {feet: 'simple-boots', 'right-hand': 'simple-sword', torso: 'leather-tunic'} 'the-first-kithmaze': {feet: 'simple-boots', 'programming-book': 'programmaticon-i'} 'haunted-kithmaze': {feet: 'simple-boots', 'programming-book': 'programmaticon-i'} 'descending-further': {feet: 'simple-boots', 'programming-book': 'programmaticon-i'} 'the-second-kithmaze': {feet: 'simple-boots', 'programming-book': 'programmaticon-i'} 'dread-door': {'right-hand': 'simple-sword', 'programming-book': 'programmaticon-i'} 'known-enemy': {'right-hand': 'simple-sword', 'programming-book': 'programmaticon-i'} 'master-of-names': {feet: 'simple-boots', 'right-hand': 'simple-sword', 'programming-book': 'programmaticon-i', eyes: 'crude-glasses'} 'lowly-kithmen': {feet: 'simple-boots', 'right-hand': 'simple-sword', 'programming-book': 'programmaticon-i', eyes: 'crude-glasses'} 'closing-the-distance': {feet: 'simple-boots', 'right-hand': 'simple-sword', torso: 'leather-tunic', eyes: 'crude-glasses'} 'tactical-strike': {feet: 'simple-boots', 'right-hand': 'simple-sword', torso: 'leather-tunic', eyes: 'crude-glasses'} 'the-final-kithmaze': {feet: 'simple-boots', 'right-hand': 'simple-sword', torso: 'leather-tunic', 'programming-book': 'programmaticon-i', eyes: 'crude-glasses'} 'the-gauntlet': {feet: 'simple-boots', 'right-hand': 'simple-sword', torso: 'leather-tunic', 'programming-book': 'programmaticon-i', eyes: 'crude-glasses'} 'kithgard-gates': {feet: 'simple-boots', 'right-hand': 'builders-hammer', torso: 'leather-tunic'} 'defense-of-plainswood': {feet: 'simple-boots', 'right-hand': 'builders-hammer'} 'winding-trail': {feet: 'leather-boots', 'right-hand': 'builders-hammer'} 'thornbush-farm': {feet: 'leather-boots', 'right-hand': 'builders-hammer', eyes: 'crude-glasses'} 'a-fiery-trap': {feet: 'leather-boots', 'right-hand': 'builders-hammer', eyes: 'crude-glasses'} 'coinucopia': {feet: 'leather-boots', flag: 'basic-flags'} restrictedGearByLevel = 'dungeons-of-kithgard': {feet: 'leather-boots'} 'gems-in-the-deep': {feet: 'leather-boots'} 'shadow-guard': {feet: 'leather-boots', 'right-hand': 'simple-sword'} 'kounter-kithwise': {feet: 'leather-boots', 'right-hand': 'simple-sword', 'programming-book': 'programmaticon-i'} 'crawlways-of-kithgard': {feet: 'leather-boots', 'right-hand': 'simple-sword', 'programming-book': 'programmaticon-i'} 'forgetful-gemsmith': {feet: 'leather-boots', 'programming-book': 'programmaticon-i'} 'true-names': {feet: 'leather-boots', 'programming-book': 'programmaticon-i'} 'favorable-odds': {feet: 'leather-boots', 'programming-book': 'programmaticon-i'} 'the-raised-sword': {feet: 'leather-boots', 'programming-book': 'programmaticon-i'} 'the-first-kithmaze': {feet: 'leather-boots'} 'haunted-kithmaze': {feet: 'leather-boots'} 'descending-further': {feet: 'leather-boots'} 'the-second-kithmaze': {feet: 'leather-boots'} 'the-final-kithmaze': {feet: 'leather-boots'} 'the-gauntlet': {feet: 'leather-boots'} 'kithgard-gates': {'right-hand': 'simple-sword'} 'defense-of-plainswood': {'right-hand': 'simple-sword'} 'winding-trail': {feet: 'simple-boots', 'right-hand': 'simple-sword'} 'thornbush-farm': {feet: 'simple-boots', 'right-hand': 'simple-sword'} 'a-fiery-trap': {feet: 'simple-boots', 'right-hand': 'builders-hammer'}