mirror of
https://github.com/codeninjasllc/codecombat.git
synced 2024-12-05 05:11:17 -05:00
712 lines
28 KiB
CoffeeScript
712 lines
28 KiB
CoffeeScript
ModalView = require 'views/core/ModalView'
|
|
template = require 'templates/play/menu/inventory-modal'
|
|
buyGemsPromptTemplate = require 'templates/play/modal/buy-gems-prompt'
|
|
{me} = require 'core/auth'
|
|
ThangType = require 'models/ThangType'
|
|
CocoCollection = require 'collections/CocoCollection'
|
|
ItemView = require './ItemView'
|
|
SpriteBuilder = require 'lib/sprites/SpriteBuilder'
|
|
ItemDetailsView = require 'views/play/modal/ItemDetailsView'
|
|
Purchase = require 'models/Purchase'
|
|
BuyGemsModal = require 'views/play/modal/BuyGemsModal'
|
|
AuthModal = require 'views/core/AuthModal'
|
|
|
|
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
|
|
ringSlots: ['left-ring', 'right-ring']
|
|
closesOnClickOutside: false # because draggable somehow triggers hide when you don't drag onto a draggable
|
|
|
|
events:
|
|
'click .item-slot': 'onItemSlotClick'
|
|
'click #unequipped .item': 'onUnequippedItemClick'
|
|
'doubletap #unequipped .item': 'onUnequippedItemDoubleClick'
|
|
'doubletap .item-slot .item': 'onEquippedItemDoubleClick'
|
|
'click button.equip-item': 'onClickEquipItemButton'
|
|
'shown.bs.modal': 'onShown'
|
|
'click #choose-hero-button': 'onClickChooseHero'
|
|
'click #play-level-button': 'onClickPlayLevel'
|
|
'click .unlock-button': 'onUnlockButtonClicked'
|
|
'click #equip-item-viewed': 'onClickEquipItemViewed'
|
|
'click #unequip-item-viewed': 'onClickUnequipItemViewed'
|
|
'click #close-modal': 'hide'
|
|
'click .buy-gems-prompt-button': 'onBuyGemsPromptButtonClicked'
|
|
'click': 'onClickedSomewhere'
|
|
'update #unequipped .nano': 'onScrollUnequipped'
|
|
|
|
shortcuts:
|
|
'esc': 'clearSelection'
|
|
'enter': 'onClickPlayLevel'
|
|
|
|
|
|
#- Setup
|
|
|
|
initialize: (options) ->
|
|
@onScrollUnequipped = _.throttle(_.bind(@onScrollUnequipped, @), 200)
|
|
super(arguments...)
|
|
@items = new CocoCollection([], {model: ThangType})
|
|
# TODO: switch to item store loading system?
|
|
@items.url = '/db/thang.type?view=items'
|
|
@items.setProjection [
|
|
'name'
|
|
'slug'
|
|
'components'
|
|
'original'
|
|
'rasterIcon'
|
|
'dollImages'
|
|
'gems'
|
|
'tier'
|
|
'description'
|
|
'heroClass'
|
|
'i18n'
|
|
]
|
|
@supermodel.loadCollection(@items, 'items')
|
|
@equipment = {} # Assign for real when we have loaded the session and items.
|
|
|
|
onItemsLoaded: ->
|
|
item.notInLevel = true for item in @items.models
|
|
@equipment = @options.equipment or @options.session?.get('heroConfig')?.inventory or me.get('heroConfig')?.inventory or {}
|
|
@equipment = $.extend true, {}, @equipment
|
|
@requireLevelEquipment()
|
|
@itemGroups = {}
|
|
@itemGroups.requiredPurchaseItems = new Backbone.Collection()
|
|
@itemGroups.availableItems = new Backbone.Collection()
|
|
@itemGroups.restrictedItems = new Backbone.Collection()
|
|
@itemGroups.lockedItems = new Backbone.Collection()
|
|
itemGroup.comparator = ((m) -> m.get('tier') ? m.get('gems')) for itemGroup in _.values @itemGroups
|
|
|
|
equipped = _.values(@equipment)
|
|
@sortItem(item, equipped) for item in @items.models
|
|
|
|
sortItem: (item, equipped) ->
|
|
equipped ?= _.values(@equipment)
|
|
|
|
# general starting classes
|
|
item.classes = _.clone(item.getAllowedSlots())
|
|
for heroClass in item.getAllowedHeroClasses()
|
|
item.classes.push heroClass
|
|
item.classes.push 'equipped' if item.get('original') in equipped
|
|
|
|
# sort into one of the four groups
|
|
locked = not (item.get('original') in me.items())
|
|
#locked = false if me.get('slug') is 'nick'
|
|
|
|
allRestrictedGear = _.flatten(_.values(@options.level.get('restrictedGear') ? {}))
|
|
restricted = item.get('original') in allRestrictedGear
|
|
|
|
# TODO: make this re-use result of computation of updateLevelRequiredItems, which we can only do after heroClass is ready...
|
|
requiredToPurchase = false
|
|
if requiredGear = @options.level.get('requiredGear')
|
|
inCampaignView = $('#campaign-view').length
|
|
unless gearSlugs[item.get('original')] is 'tarnished-bronze-breastplate' and inCampaignView and @options.level.get('slug') is 'the-raised-sword'
|
|
for slot in item.getAllowedSlots()
|
|
continue unless requiredItems = requiredGear[slot]
|
|
continue if @equipment[slot] and @equipment[slot] not in allRestrictedGear and slot not in @ringSlots
|
|
# Point out that they must buy it if they haven't bought any of the required items for that slot, and it's the first one.
|
|
if item.get('original') is requiredItems[0] and not _.find(requiredItems, (requiredItem) -> me.ownsItem requiredItem)
|
|
requiredToPurchase = true
|
|
break
|
|
|
|
if requiredToPurchase and locked and not item.get('gems')
|
|
# Either one of two things has happened:
|
|
# 1. There's a bug and the player doesn't have a required item they should have.
|
|
# 2. The player is trying to play a level they haven't unlocked.
|
|
# We'll just pretend they own it so that they don't get stuck.
|
|
application.tracker?.trackEvent 'Required Item Locked', level: @options.level.get('slug'), label: @options.level.get('slug'), item: item.get('name'), playerLevel: me.level(), levelUnlocked: me.ownsLevel @options.level.get('original')
|
|
locked = false
|
|
|
|
placeholder = not item.getFrontFacingStats().props.length and not _.size(item.getFrontFacingStats().stats)
|
|
|
|
if placeholder and locked # The item is not complete, so don't put it into a collection.
|
|
null
|
|
else if locked and requiredToPurchase
|
|
item.classes.push 'locked'
|
|
@itemGroups.requiredPurchaseItems.add item
|
|
else if locked
|
|
item.classes.push 'locked'
|
|
if item.isSilhouettedItem() or not item.get('gems')
|
|
# Don't even load/show these--don't add to a collection. (Bandwidth optimization.)
|
|
null
|
|
else
|
|
@itemGroups.lockedItems.add(item)
|
|
else if restricted
|
|
@itemGroups.restrictedItems.add(item)
|
|
item.classes.push 'restricted'
|
|
else
|
|
@itemGroups.availableItems.add(item)
|
|
|
|
# level to unlock
|
|
item.level = item.levelRequiredForItem() if item.get('tier')?
|
|
|
|
onLoaded: ->
|
|
# Both items and session have been loaded.
|
|
@onItemsLoaded()
|
|
super()
|
|
|
|
getRenderData: (context={}) ->
|
|
context = super(context)
|
|
context.equipped = _.values(@equipment)
|
|
context.items = @items.models
|
|
context.itemGroups = @itemGroups
|
|
context.slots = @slots
|
|
context.selectedHero = @selectedHero
|
|
context.selectedHeroClass = @selectedHero?.get('heroClass')
|
|
context.equipment = _.clone @equipment
|
|
context.equipment[slot] = @items.findWhere {original: itemOriginal} for slot, itemOriginal of context.equipment
|
|
context.gems = me.gems()
|
|
context
|
|
|
|
afterRender: ->
|
|
super()
|
|
@$el.find('#play-level-button').css('visibility', 'hidden')
|
|
return unless @supermodel.finished()
|
|
@$el.find('#play-level-button').css('visibility', 'visible')
|
|
|
|
@setUpDraggableEventsForAvailableEquipment()
|
|
@setUpDraggableEventsForEquippedArea()
|
|
@delegateEvents()
|
|
@itemDetailsView = new ItemDetailsView()
|
|
@insertSubView(@itemDetailsView)
|
|
@requireLevelEquipment()
|
|
@$el.find('.nano').nanoScroller({alwaysVisible: true})
|
|
@onSelectionChanged()
|
|
@onEquipmentChanged()
|
|
|
|
afterInsert: ->
|
|
super()
|
|
@canvasWidth = @$el.find('canvas').innerWidth()
|
|
@canvasHeight = @$el.find('canvas').innerHeight()
|
|
@inserted = true
|
|
@requireLevelEquipment()
|
|
|
|
#- Draggable logic
|
|
|
|
setUpDraggableEventsForAvailableEquipment: ->
|
|
for availableItemEl in @$el.find('#unequipped .item')
|
|
availableItemEl = $(availableItemEl)
|
|
continue if availableItemEl.hasClass('locked') or availableItemEl.hasClass('restricted')
|
|
dragHelper = availableItemEl.clone().addClass('draggable-item')
|
|
do (dragHelper, availableItemEl) =>
|
|
availableItemEl.draggable
|
|
revert: 'invalid'
|
|
appendTo: @$el
|
|
cursorAt: {left: 35.5, top: 35.5}
|
|
helper: -> dragHelper
|
|
revertDuration: 200
|
|
distance: 10
|
|
scroll: false
|
|
zIndex: 1100
|
|
availableItemEl.on 'dragstart', => @selectUnequippedItem(availableItemEl)
|
|
|
|
setUpDraggableEventsForEquippedArea: ->
|
|
for itemSlot in @$el.find '.item-slot'
|
|
slot = $(itemSlot).data 'slot'
|
|
do (slot, itemSlot) =>
|
|
$(itemSlot).droppable
|
|
drop: (e, ui) => @equipSelectedItem()
|
|
accept: (el) -> $(el).parent().hasClass slot
|
|
activeClass: 'droppable'
|
|
hoverClass: 'droppable-hover'
|
|
tolerance: 'touch'
|
|
@makeEquippedSlotDraggable $(itemSlot)
|
|
|
|
@$el.find('#equipped').droppable
|
|
drop: (e, ui) => @equipSelectedItem()
|
|
accept: (el) -> true
|
|
activeClass: 'droppable'
|
|
hoverClass: 'droppable-hover'
|
|
tolerance: 'pointer'
|
|
|
|
makeEquippedSlotDraggable: (slot) ->
|
|
unequip = =>
|
|
itemEl = @unequipItemFromSlot slot
|
|
selectedSlotItemID = itemEl.data('item-id')
|
|
item = @items.get(selectedSlotItemID)
|
|
@requireLevelEquipment()
|
|
@showItemDetails(item, 'equip')
|
|
@onSelectionChanged()
|
|
@onEquipmentChanged()
|
|
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
|
|
slot.on 'dragstart', => @selectItemSlot(slot)
|
|
|
|
|
|
#- Select/equip event handlers
|
|
|
|
onItemSlotClick: (e) ->
|
|
return if @remainingRequiredEquipment?.length # Don't let them select a slot if we need them to first equip some require gear.
|
|
#@playSound 'menu-button-click'
|
|
@selectItemSlot($(e.target).closest('.item-slot'))
|
|
|
|
onUnequippedItemClick: (e) ->
|
|
return if @justDoubleClicked
|
|
return if @justClickedEquipItemButton
|
|
itemEl = $(e.target).closest('.item')
|
|
#@playSound 'menu-button-click'
|
|
@selectUnequippedItem(itemEl)
|
|
|
|
onUnequippedItemDoubleClick: (e) ->
|
|
itemEl = $(e.target).closest('.item')
|
|
return if itemEl.hasClass('locked') or itemEl.hasClass('restricted')
|
|
@equipSelectedItem()
|
|
@justDoubleClicked = true
|
|
_.defer => @justDoubleClicked = false
|
|
|
|
onEquippedItemDoubleClick: -> @unequipSelectedItem()
|
|
onClickEquipItemViewed: -> @equipSelectedItem()
|
|
onClickUnequipItemViewed: -> @unequipSelectedItem()
|
|
|
|
onClickEquipItemButton: (e) ->
|
|
@playSound 'menu-button-click'
|
|
itemEl = $(e.target).closest('.item')
|
|
@selectUnequippedItem(itemEl)
|
|
@equipSelectedItem()
|
|
@justClickedEquipItemButton = true
|
|
_.defer => @justClickedEquipItemButton = false
|
|
|
|
#- Select/equip higher-level, all encompassing methods the callbacks all use
|
|
|
|
selectItemSlot: (slotEl) ->
|
|
@clearSelection()
|
|
slotEl.addClass('selected')
|
|
selectedSlotItemID = slotEl.find('.item').data('item-id')
|
|
item = @items.get(selectedSlotItemID)
|
|
if item then @showItemDetails(item, 'unequip')
|
|
@onSelectionChanged()
|
|
|
|
selectUnequippedItem: (itemEl) ->
|
|
@clearSelection()
|
|
itemEl.addClass('active')
|
|
showExtra = if itemEl.hasClass('restricted') then 'restricted' else if not itemEl.hasClass('locked') then 'equip' else ''
|
|
@showItemDetails(@items.get(itemEl.data('item-id')), showExtra)
|
|
@onSelectionChanged()
|
|
|
|
equipSelectedItem: ->
|
|
selectedItemEl = @getSelectedUnequippedItem()
|
|
selectedItem = @items.get(selectedItemEl.data('item-id'))
|
|
return unless selectedItem
|
|
allowedSlots = selectedItem.getAllowedSlots()
|
|
firstSlot = unequippedSlot = null
|
|
for allowedSlot in allowedSlots
|
|
slotEl = @$el.find(".item-slot[data-slot='#{allowedSlot}']")
|
|
firstSlot ?= slotEl
|
|
unequippedSlot ?= slotEl unless slotEl.find('img').length
|
|
slotEl = unequippedSlot ? firstSlot
|
|
selectedItemEl.effect('transfer', to: slotEl, duration: 500, easing: 'easeOutCubic')
|
|
unequipped = @unequipItemFromSlot(slotEl)
|
|
selectedItemEl.addClass('equipped')
|
|
slotEl.append(selectedItemEl.find('img').clone().addClass('item').data('item-id', selectedItem.id))
|
|
@clearSelection()
|
|
@showItemDetails(selectedItem, 'unequip')
|
|
slotEl.addClass('selected')
|
|
selectedItem.classes.push 'equipped'
|
|
@makeEquippedSlotDraggable slotEl
|
|
@requireLevelEquipment()
|
|
@onSelectionChanged()
|
|
@onEquipmentChanged()
|
|
|
|
unequipSelectedItem: ->
|
|
slotEl = @getSelectedSlot()
|
|
@clearSelection()
|
|
itemEl = @unequipItemFromSlot(slotEl)
|
|
return unless itemEl
|
|
itemEl.addClass('active')
|
|
slotEl.effect('transfer', to: itemEl, duration: 500, easing: 'easeOutCubic')
|
|
selectedSlotItemID = itemEl.data('item-id')
|
|
item = @items.get(selectedSlotItemID)
|
|
item.classes = _.without item.classes, 'equipped'
|
|
@showItemDetails(item, 'equip')
|
|
@requireLevelEquipment()
|
|
@onSelectionChanged()
|
|
@onEquipmentChanged()
|
|
|
|
#- Select/equip helpers
|
|
|
|
clearSelection: ->
|
|
@deselectAllSlots()
|
|
@deselectAllUnequippedItems()
|
|
@hideItemDetails()
|
|
|
|
unequipItemFromSlot: (slotEl) ->
|
|
itemEl = slotEl.find('.item')
|
|
itemIDToUnequip = itemEl.data('item-id')
|
|
return unless itemIDToUnequip
|
|
itemEl.remove()
|
|
item = @items.get itemIDToUnequip
|
|
item.classes = _.without item.classes, 'equipped'
|
|
@$el.find("#unequipped .item[data-item-id=#{itemIDToUnequip}]").removeClass('equipped')
|
|
|
|
deselectAllSlots: ->
|
|
@$el.find('#equipped .item-slot.selected').removeClass('selected')
|
|
|
|
deselectAllUnequippedItems: ->
|
|
@$el.find('#unequipped .item').removeClass('active')
|
|
|
|
getSlot: (name) ->
|
|
@$el.find(".item-slot[data-slot=#{name}]")
|
|
|
|
getSelectedSlot: ->
|
|
@$el.find('#equipped .item-slot.selected')
|
|
|
|
getSelectedUnequippedItem: ->
|
|
@$el.find('#unequipped .item.active')
|
|
|
|
onSelectionChanged: ->
|
|
heroClass = @selectedHero?.get('heroClass')
|
|
itemsCanBeEquipped = @$el.find('#unequipped .item.available:not(.equipped)').filter('.'+heroClass).length
|
|
toShow = @$el.find('#double-click-hint, #available-description')
|
|
if itemsCanBeEquipped then toShow.removeClass('secret') else toShow.addClass('secret')
|
|
@delegateEvents()
|
|
|
|
|
|
showItemDetails: (item, showExtra) ->
|
|
@itemDetailsView.setItem(item)
|
|
@$el.find('#item-details-extra > *').addClass('secret')
|
|
@$el.find("##{showExtra}-item-viewed").removeClass('secret')
|
|
|
|
hideItemDetails: ->
|
|
@itemDetailsView?.setItem(null)
|
|
@$el.find('#item-details-extra > *').addClass('secret')
|
|
|
|
getCurrentEquipmentConfig: ->
|
|
config = {}
|
|
for slot in @$el.find('.item-slot')
|
|
slotName = $(slot).data('slot')
|
|
slotItemID = $(slot).find('.item').data('item-id')
|
|
continue unless slotItemID
|
|
item = _.find @items.models, {id:slotItemID}
|
|
config[slotName] = item.get('original')
|
|
config
|
|
|
|
requireLevelEquipment: ->
|
|
# This is called frequently to make sure the player isn't using any restricted items and knows she must equip any required items.
|
|
return unless @inserted
|
|
equipment = if @supermodel.finished() then @getCurrentEquipmentConfig() else @equipment # Make sure we're using latest equipment.
|
|
hadRequired = @remainingRequiredEquipment?.length
|
|
@remainingRequiredEquipment = []
|
|
@$el.find('.should-equip').removeClass('should-equip')
|
|
@unequipClassRestrictedItems equipment
|
|
@unequipLevelRestrictedItems equipment
|
|
@updateLevelRequiredItems equipment
|
|
if hadRequired and not @remainingRequiredEquipment.length
|
|
@endHighlight()
|
|
@highlightElement '#play-level-button', duration: 5000
|
|
$('#play-level-button').prop('disabled', @remainingRequiredEquipment.length > 0)
|
|
|
|
unequipClassRestrictedItems: (equipment) ->
|
|
return unless @supermodel.finished() and heroClass = @selectedHero?.get 'heroClass'
|
|
for slot, item of _.clone equipment
|
|
itemModel = @items.findWhere original: item
|
|
unless itemModel and heroClass in itemModel.classes
|
|
console.log 'Unequipping', itemModel.get('heroClass'), 'item', itemModel.get('name'), 'from slot due to class restrictions.'
|
|
@unequipItemFromSlot @$el.find(".item-slot[data-slot='#{slot}']")
|
|
delete equipment[slot]
|
|
|
|
unequipLevelRestrictedItems: (equipment) ->
|
|
return unless restrictedGear = @options.level.get 'restrictedGear'
|
|
for slot, items of restrictedGear
|
|
for item in items
|
|
equipped = equipment[slot]
|
|
if equipped and equipped is item
|
|
console.log 'Unequipping restricted item', equipped, 'for', slot, 'before level', @options.level.get('slug')
|
|
@unequipItemFromSlot @$el.find(".item-slot[data-slot='#{slot}']")
|
|
delete equipment[slot]
|
|
|
|
updateLevelRequiredItems: (equipment) ->
|
|
return unless requiredGear = @options.level.get 'requiredGear'
|
|
return unless heroClass = @selectedHero?.get 'heroClass'
|
|
|
|
for slot, items of requiredGear when items.length
|
|
if slot in @ringSlots
|
|
validSlots = @ringSlots
|
|
else
|
|
validSlots = [slot]
|
|
|
|
continue if validSlots.some (slot) ->
|
|
equipped = equipment[slot]
|
|
equipped in items
|
|
|
|
# Actually, just let them play if they have equipped anything in that slot (and we haven't unequipped it due to restrictions).
|
|
# Rings often have unique effects, so this rule does not apply to them (they are still required even if there is a non-restricted ring equipped in the slot).
|
|
continue if equipment[slot] and slot not in @ringSlots
|
|
|
|
items = (item for item in items when heroClass in (@items.findWhere(original: item)?.classes ? []))
|
|
continue unless items.length # If the required items are for another class, then let's not be finicky.
|
|
|
|
# We will point out the last (best) element that they own and can use, otherwise the first (cheapest).
|
|
bestOwnedItem = _.findLast items, (item) -> me.ownsItem item
|
|
item = bestOwnedItem ? items[0]
|
|
|
|
# For the Tarnished Bronze Breastplate only, don't tell them they need it until they need it in the level, so we can show how to buy it.
|
|
slug = gearSlugs[item]
|
|
inCampaignView = $('#campaign-view').length
|
|
continue if slug is 'tarnished-bronze-breastplate' and inCampaignView and @options.level.get('slug') is 'the-raised-sword'
|
|
|
|
# Now we're definitely requiring and pointing out an item.
|
|
itemModel = @items.findWhere {original: item}
|
|
availableSlotSelector = "#unequipped .item[data-item-id='#{itemModel.id}']"
|
|
@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: item
|
|
null
|
|
|
|
setHero: (@selectedHero) ->
|
|
if @selectedHero.loading
|
|
@listenToOnce @selectedHero, 'sync', => @setHero? @selectedHero
|
|
return
|
|
@$el.removeClass('Warrior Ranger Wizard').addClass(@selectedHero.get('heroClass'))
|
|
@requireLevelEquipment()
|
|
@render()
|
|
@onEquipmentChanged()
|
|
|
|
onShown: ->
|
|
# Called when we switch tabs to this within the modal
|
|
@requireLevelEquipment()
|
|
|
|
onHidden: ->
|
|
# Called when the modal itself is dismissed
|
|
@endHighlight()
|
|
super()
|
|
@playSound 'game-menu-close'
|
|
|
|
onClickChooseHero: ->
|
|
@playSound 'menu-button-click'
|
|
@hide()
|
|
@trigger 'choose-hero-click'
|
|
|
|
onClickPlayLevel: (e) ->
|
|
return if @$el.find('#play-level-button').prop 'disabled'
|
|
levelSlug = @options.level.get('slug')
|
|
@playSound 'menu-button-click'
|
|
@showLoading()
|
|
ua = navigator.userAgent.toLowerCase()
|
|
isSafari = /safari/.test(ua) and not /chrome/.test(ua)
|
|
isTooShort = $(window).height() < 658 # Min vertical resolution needed at 1366px wide
|
|
if isTooShort and not me.isAdmin() and not hasGoneFullScreenOnce and not isSafari
|
|
@toggleFullscreen()
|
|
hasGoneFullScreenOnce = true
|
|
@updateConfig =>
|
|
@trigger? 'play-click'
|
|
window.tracker?.trackEvent 'Inventory Play', category: 'Play Level', level: levelSlug
|
|
|
|
updateConfig: (callback, skipSessionSave) ->
|
|
sessionHeroConfig = @options.session.get('heroConfig') ? {}
|
|
lastHeroConfig = me.get('heroConfig') ? {}
|
|
inventory = @getCurrentEquipmentConfig()
|
|
patchSession = patchMe = false
|
|
patchSession ||= not _.isEqual inventory, sessionHeroConfig.inventory
|
|
sessionHeroConfig.inventory = inventory
|
|
if hero = @selectedHero?.get('original')
|
|
patchSession ||= not _.isEqual hero, sessionHeroConfig.thangType
|
|
sessionHeroConfig.thangType = hero
|
|
patchMe ||= not _.isEqual inventory, lastHeroConfig.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?()
|
|
|
|
#- TODO: DRY this between PlayItemsModal and InventoryModal and PlayHeroesModal
|
|
|
|
onUnlockButtonClicked: (e) ->
|
|
e.stopPropagation()
|
|
button = $(e.target).closest('button')
|
|
item = @items.get(button.data('item-id'))
|
|
affordable = item.affordable
|
|
if not affordable
|
|
@playSound 'menu-button-click'
|
|
@askToBuyGems button
|
|
else if button.hasClass('confirm')
|
|
@playSound 'menu-button-unlock-end'
|
|
purchase = Purchase.makeFor(item)
|
|
purchase.save()
|
|
|
|
#- set local changes to mimic what should happen on the server...
|
|
purchased = me.get('purchased') ? {}
|
|
purchased.items ?= []
|
|
purchased.items.push(item.get('original'))
|
|
|
|
me.set('purchased', purchased)
|
|
me.set('spent', (me.get('spent') ? 0) + item.get('gems'))
|
|
|
|
#- ...then rerender key bits
|
|
@itemGroups.lockedItems.remove(item)
|
|
@itemGroups.requiredPurchaseItems.remove(item)
|
|
# Redo all item sorting to make sure that we don't clobber state changes since last render.
|
|
equipped = _.values @getCurrentEquipmentConfig()
|
|
@sortItem(otherItem, equipped) for otherItem in @items.models
|
|
@renderSelectors('#unequipped', '#gems-count')
|
|
|
|
@requireLevelEquipment()
|
|
@delegateEvents()
|
|
@setUpDraggableEventsForAvailableEquipment()
|
|
@itemDetailsView.setItem(item)
|
|
@onScrollUnequipped true
|
|
|
|
Backbone.Mediator.publish 'store:item-purchased', item: item, itemSlug: item.get('slug')
|
|
else
|
|
@playSound 'menu-button-unlock-start'
|
|
button.addClass('confirm').text($.i18n.t('play.confirm'))
|
|
@$el.one 'click', (e) ->
|
|
button.removeClass('confirm').text($.i18n.t('play.unlock')) if e.target isnt button[0]
|
|
|
|
askToSignUp: ->
|
|
authModal = new AuthModal supermodel: @supermodel
|
|
authModal.mode = 'signup'
|
|
return @openModalView authModal
|
|
|
|
askToBuyGems: (unlockButton) ->
|
|
@$el.find('.unlock-button').popover 'destroy'
|
|
popoverTemplate = buyGemsPromptTemplate {}
|
|
unlockButton.popover(
|
|
animation: true
|
|
trigger: 'manual'
|
|
placement: 'top'
|
|
content: ' ' # template has it
|
|
container: @$el
|
|
template: popoverTemplate
|
|
).popover 'show'
|
|
popover = unlockButton.data('bs.popover')
|
|
popover?.$tip?.i18n()
|
|
|
|
onBuyGemsPromptButtonClicked: (e) ->
|
|
@playSound 'menu-button-click'
|
|
return @askToSignUp() if me.get('anonymous')
|
|
@openModalView new BuyGemsModal()
|
|
|
|
onClickedSomewhere: (e) ->
|
|
return if @destroyed
|
|
@$el.find('.unlock-button').popover 'destroy'
|
|
|
|
|
|
#- Dynamic portrait loading
|
|
|
|
onScrollUnequipped: (forceLoadAll=false) ->
|
|
# dynamically load visible items when the user scrolls enough to see them
|
|
return if @destroyed
|
|
nanoContent = @$el.find('#unequipped .nano-content')
|
|
items = nanoContent.find('.item:visible:not(.loaded)')
|
|
threshold = nanoContent.height() + 100
|
|
for itemEl in items
|
|
itemEl = $(itemEl)
|
|
if itemEl.position().top < threshold or forceLoadAll
|
|
itemEl.addClass('loaded')
|
|
item = @items.get(itemEl.data('item-id'))
|
|
itemEl.find('img').attr('src', item.getPortraitURL())
|
|
|
|
|
|
#- Paper doll equipment updating
|
|
onEquipmentChanged: ->
|
|
heroClass = @selectedHero?.get('heroClass') ? 'Warrior'
|
|
gender = if @selectedHero?.get('slug') in heroGenders.male then 'male' else 'female'
|
|
@$el.find('#hero-image, #hero-image-hair, #hero-image-head, #hero-image-thumb').removeClass().addClass "#{gender} #{heroClass}"
|
|
equipment = @getCurrentEquipmentConfig()
|
|
@onScrollUnequipped()
|
|
return unless _.size(equipment) and @supermodel.finished()
|
|
@removeDollImages()
|
|
slotsWithImages = []
|
|
for slot, original of equipment
|
|
item = _.find @items.models, (item) -> item.get('original') is original
|
|
continue unless dollImages = item?.get('dollImages')
|
|
didAdd = @addDollImage slot, dollImages, heroClass, gender, item
|
|
slotsWithImages.push slot if didAdd if item.get('original') isnt '54ea39342b7506e891ca70f2' # Circlet of the Magi needs hair under it
|
|
@$el.find('#hero-image-hair').toggle not ('head' in slotsWithImages)
|
|
@$el.find('#hero-image-thumb').toggle not ('gloves' in slotsWithImages)
|
|
|
|
@equipment = @options.equipment = equipment
|
|
|
|
removeDollImages: ->
|
|
@$el.find('.doll-image').remove()
|
|
|
|
addDollImage: (slot, dollImages, heroClass, gender, item) ->
|
|
heroClass = @selectedHero?.get('heroClass') ? 'Warrior'
|
|
gender = if @selectedHero?.get('slug') in heroGenders.male then 'male' else 'female'
|
|
didAdd = false
|
|
if slot is 'gloves'
|
|
if heroClass is 'Ranger'
|
|
imageKeys = ["#{gender}#{heroClass}", "#{gender}#{heroClass}Thumb"]
|
|
else
|
|
imageKeys = ["#{gender}", "#{gender}Thumb"]
|
|
else if heroClass is 'Wizard' and slot is 'torso'
|
|
imageKeys = [gender, "#{gender}Back"]
|
|
else if heroClass is 'Ranger' and slot is 'head' and item.get('original') in ['5441c2be4e9aeb727cc97105', '5441c3144e9aeb727cc97111']
|
|
# All-class headgear like faux fur hat, viking helmet is abusing ranger glove slot
|
|
imageKeys = ["#{gender}Ranger"]
|
|
else
|
|
imageKeys = [gender]
|
|
for imageKey in imageKeys
|
|
imageURL = dollImages[imageKey]
|
|
if not imageURL
|
|
console.log "Hmm, should have #{slot} #{imageKey} paper doll image, but don't have it."
|
|
else
|
|
imageEl = $('<img>').attr('src', "/file/#{imageURL}").addClass("doll-image #{slot} #{heroClass} #{gender} #{_.string.underscored(imageKey).replace(/_/g, '-')}").attr('draggable', false)
|
|
@$el.find('#equipped').append imageEl
|
|
didAdd = true
|
|
didAdd
|
|
|
|
destroy: ->
|
|
@$el.find('.unlock-button').popover 'destroy'
|
|
@$el.find('.ui-droppable').droppable 'destroy'
|
|
@$el.find('.ui-draggable').draggable('destroy').off 'dragstart'
|
|
@$el.find('.item-slot').off 'dragstart'
|
|
@stage?.removeAllChildren()
|
|
super()
|
|
|
|
|
|
heroGenders =
|
|
male: ['knight', 'samurai', 'trapper', 'potion-master', 'goliath', 'assassin', 'necromancer']
|
|
female: ['captain', 'ninja', 'forest-archer', 'librarian', 'sorcerer', 'raider', 'guardian', 'pixie', 'dark-wizard']
|
|
|
|
gear =
|
|
'simple-boots': '53e237bf53457600003e3f05'
|
|
'simple-sword': '53e218d853457600003e3ebe'
|
|
'tarnished-bronze-breastplate': '53e22eac53457600003e3efc'
|
|
'leather-boots': '53e2384453457600003e3f07'
|
|
'leather-belt': '5437002a7beba4a82024a97d'
|
|
'programmaticon-i': '53e4108204c00d4607a89f78'
|
|
'programmaticon-ii': '546e25d99df4a17d0d449be1'
|
|
'crude-glasses': '53e238df53457600003e3f0b'
|
|
'crude-builders-hammer': '53f4e6e3d822c23505b74f42'
|
|
'long-sword': '544d7d1f8494308424f564a3'
|
|
'sundial-wristwatch': '53e2396a53457600003e3f0f'
|
|
'bronze-shield': '544c310ae0017993fce214bf'
|
|
'wooden-glasses': '53e2167653457600003e3eb3'
|
|
'basic-flags': '545bacb41e649a4495f887da'
|
|
'roughedge': '544d7d918494308424f564a7'
|
|
'sharpened-sword': '544d7deb8494308424f564ab'
|
|
'crude-crossbow': '544d7ffd8494308424f564c3'
|
|
'crude-dagger': '544d952b8494308424f56517'
|
|
'weak-charge': '544d957d8494308424f5651f'
|
|
'enchanted-stick': '544d87188494308424f564f1'
|
|
'unholy-tome-i': '546374bc3839c6e02811d308'
|
|
'book-of-life-i': '546375653839c6e02811d30b'
|
|
'rough-sense-stone': '54693140a2b1f53ce79443bc'
|
|
'polished-sense-stone': '53e215a253457600003e3eaf'
|
|
'quartz-sense-stone': '54693240a2b1f53ce79443c5'
|
|
'wooden-builders-hammer': '54694ba3a2b1f53ce794444d'
|
|
'simple-wristwatch': '54693797a2b1f53ce79443e9'
|
|
|
|
gearSlugs = _.invert gear
|