mirror of
https://github.com/codeninjasllc/codecombat.git
synced 2024-11-28 18:15:52 -05:00
7d7d2d5f0e
Rings often have unique effects instead of progressively enhancing another ring's effect, hence let's make rings required even if there's already non-restricted rings equipped. If, by any chance, there's ever a new ring which enhances the effect of a ring which is required for some level, then it should be easy enough to whitelist it in the given ring slot's allowed items array.
709 lines
28 KiB
CoffeeScript
709 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()
|
|
|
|
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()
|
|
unless me.isAdmin() or hasGoneFullScreenOnce and (/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 '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
|