2014-11-28 20:49:41 -05:00
|
|
|
CocoView = require 'views/core/CocoView'
|
2014-01-03 13:32:13 -05:00
|
|
|
template = require 'templates/play/level/tome/spell_palette'
|
2014-11-28 20:49:41 -05:00
|
|
|
{me} = require 'core/auth'
|
2014-01-03 13:32:13 -05:00
|
|
|
filters = require 'lib/image_filter'
|
2014-07-23 10:02:45 -04:00
|
|
|
SpellPaletteEntryView = require './SpellPaletteEntryView'
|
2014-02-12 19:42:09 -05:00
|
|
|
LevelComponent = require 'models/LevelComponent'
|
2014-10-17 00:38:11 -04:00
|
|
|
ThangType = require 'models/ThangType'
|
2014-11-10 15:47:24 -05:00
|
|
|
LevelOptions = require 'lib/LevelOptions'
|
2014-12-18 03:19:40 -05:00
|
|
|
GameMenuModal = require 'views/play/menu/GameMenuModal'
|
2014-01-03 13:32:13 -05:00
|
|
|
|
2014-02-16 18:30:00 -05:00
|
|
|
N_ROWS = 4
|
|
|
|
|
2014-07-17 20:20:11 -04:00
|
|
|
module.exports = class SpellPaletteView extends CocoView
|
2014-01-03 13:32:13 -05:00
|
|
|
id: 'spell-palette-view'
|
|
|
|
template: template
|
|
|
|
controlsEnabled: true
|
|
|
|
|
|
|
|
subscriptions:
|
2014-08-27 15:24:03 -04:00
|
|
|
'level:disable-controls': 'onDisableControls'
|
|
|
|
'level:enable-controls': 'onEnableControls'
|
2014-06-30 22:16:26 -04:00
|
|
|
'surface:frame-changed': 'onFrameChanged'
|
2014-06-25 00:07:36 -04:00
|
|
|
'tome:change-language': 'onTomeChangedLanguage'
|
|
|
|
|
2014-12-18 03:19:40 -05:00
|
|
|
events:
|
|
|
|
'click #spell-palette-help-button': 'onClickHelp'
|
|
|
|
|
2014-01-03 13:32:13 -05:00
|
|
|
constructor: (options) ->
|
|
|
|
super options
|
2014-12-18 03:19:40 -05:00
|
|
|
@level = options.level
|
|
|
|
@session = options.session
|
|
|
|
@supermodel = options.supermodel
|
2014-01-03 13:32:13 -05:00
|
|
|
@thang = options.thang
|
2014-02-16 18:30:00 -05:00
|
|
|
@createPalette()
|
2014-10-17 00:38:11 -04:00
|
|
|
$(window).on 'resize', @onResize
|
2014-02-16 18:30:00 -05:00
|
|
|
|
|
|
|
getRenderData: ->
|
|
|
|
c = super()
|
|
|
|
c.entryGroups = @entryGroups
|
|
|
|
c.entryGroupSlugs = @entryGroupSlugs
|
2014-06-25 23:19:11 -04:00
|
|
|
c.entryGroupNames = @entryGroupNames
|
2014-02-16 18:30:00 -05:00
|
|
|
c.tabbed = _.size(@entryGroups) > 1
|
2014-03-13 12:02:19 -04:00
|
|
|
c.defaultGroupSlug = @defaultGroupSlug
|
2014-02-16 18:30:00 -05:00
|
|
|
c
|
2014-01-03 13:32:13 -05:00
|
|
|
|
|
|
|
afterRender: ->
|
|
|
|
super()
|
2014-10-17 00:38:11 -04:00
|
|
|
if @entryGroupSlugs
|
|
|
|
for group, entries of @entryGroups
|
|
|
|
groupSlug = @entryGroupSlugs[group]
|
|
|
|
for columnNumber, entryColumn of entries
|
|
|
|
col = $('<div class="property-entry-column"></div>').appendTo @$el.find(".properties-#{groupSlug}")
|
|
|
|
for entry in entryColumn
|
|
|
|
col.append entry.el
|
|
|
|
entry.render() # Render after appending so that we can access parent container for popover
|
|
|
|
$('.nano').nanoScroller()
|
|
|
|
@updateCodeLanguage @options.language
|
|
|
|
else
|
|
|
|
@entryGroupElements = {}
|
|
|
|
for group, entries of @entryGroups
|
|
|
|
@entryGroupElements[group] = itemGroup = $('<div class="property-entry-item-group"></div>').appendTo @$el.find('.properties')
|
2014-11-19 21:36:09 -05:00
|
|
|
if entries[0].options.item?.getPortraitURL
|
|
|
|
itemImage = $('<img class="item-image" draggable=false></img>').attr('src', entries[0].options.item.getPortraitURL()).css('top', Math.max(0, 19 * (entries.length - 2) / 2) + 2)
|
|
|
|
itemGroup.append itemImage
|
|
|
|
firstEntry = entries[0]
|
|
|
|
do (firstEntry) ->
|
|
|
|
itemImage.on "mouseenter", (e) -> firstEntry.onMouseEnter e
|
|
|
|
itemImage.on "mouseleave", (e) -> firstEntry.onMouseLeave e
|
|
|
|
for entry, entryIndex in entries
|
2014-10-17 00:38:11 -04:00
|
|
|
itemGroup.append entry.el
|
2014-02-16 18:30:00 -05:00
|
|
|
entry.render() # Render after appending so that we can access parent container for popover
|
2014-11-19 21:36:09 -05:00
|
|
|
if entries.length is 1
|
|
|
|
entry.$el.addClass 'single-entry'
|
|
|
|
if entryIndex is 0
|
|
|
|
entry.$el.addClass 'first-entry'
|
2014-10-17 00:38:11 -04:00
|
|
|
@$el.addClass 'hero'
|
2014-10-18 09:46:07 -04:00
|
|
|
@updateMaxHeight() unless application.isIPadApp
|
2014-06-25 00:07:36 -04:00
|
|
|
|
2014-10-16 15:08:21 -04:00
|
|
|
afterInsert: ->
|
|
|
|
super()
|
|
|
|
_.delay => @$el?.css('bottom', 0) unless $('#spell-view').is('.shown')
|
|
|
|
|
2014-06-25 00:07:36 -04:00
|
|
|
updateCodeLanguage: (language) ->
|
|
|
|
@options.language = language
|
2014-01-03 13:32:13 -05:00
|
|
|
|
2014-10-17 00:38:11 -04:00
|
|
|
updateMaxHeight: ->
|
|
|
|
return unless @isHero
|
|
|
|
nColumns = Math.floor @$el.find('.properties').innerWidth() / 212 # ~212px is a good max entry width; will always have 2 columns
|
|
|
|
columns = ({items: [], nEntries: 0} for i in [0 ... nColumns])
|
|
|
|
nRows = 0
|
|
|
|
for group, entries of @entryGroups
|
2014-10-23 19:36:59 -04:00
|
|
|
continue unless shortestColumn = _.sortBy(columns, (column) -> column.nEntries)[0]
|
2014-10-17 00:38:11 -04:00
|
|
|
shortestColumn.nEntries += Math.max 2, entries.length
|
|
|
|
shortestColumn.items.push @entryGroupElements[group]
|
|
|
|
nRows = Math.max nRows, shortestColumn.nEntries
|
|
|
|
for column in columns
|
|
|
|
for item in column.items
|
|
|
|
item.detach().appendTo @$el.find('.properties')
|
|
|
|
@$el.find('.properties').css('height', 19 * (nRows + 1))
|
|
|
|
|
|
|
|
onResize: (e) =>
|
|
|
|
@updateMaxHeight()
|
|
|
|
|
2014-01-03 13:32:13 -05:00
|
|
|
createPalette: ->
|
2014-09-08 15:29:49 -04:00
|
|
|
Backbone.Mediator.publish 'tome:palette-cleared', {thangID: @thang.id}
|
2014-02-12 19:42:09 -05:00
|
|
|
lcs = @supermodel.getModels LevelComponent
|
|
|
|
allDocs = {}
|
2014-07-15 21:54:55 -04:00
|
|
|
excludedDocs = {}
|
2014-02-18 14:23:01 -05:00
|
|
|
for lc in lcs
|
|
|
|
for doc in (lc.get('propertyDocumentation') ? [])
|
2014-07-15 21:54:55 -04:00
|
|
|
if doc.codeLanguages and not (@options.language in doc.codeLanguages)
|
2014-07-15 22:25:53 -04:00
|
|
|
excludedDocs['__' + doc.name] = doc
|
2014-07-15 21:54:55 -04:00
|
|
|
continue
|
2014-02-28 15:43:31 -05:00
|
|
|
allDocs['__' + doc.name] ?= []
|
|
|
|
allDocs['__' + doc.name].push doc
|
2014-02-24 18:48:30 -05:00
|
|
|
if doc.type is 'snippet' then doc.owner = 'snippets'
|
2014-01-03 13:32:13 -05:00
|
|
|
|
2014-04-25 19:57:42 -04:00
|
|
|
if @options.programmable
|
|
|
|
propStorage =
|
|
|
|
'this': 'programmableProperties'
|
|
|
|
more: 'moreProgrammableProperties'
|
|
|
|
Math: 'programmableMathProperties'
|
|
|
|
Array: 'programmableArrayProperties'
|
|
|
|
Object: 'programmableObjectProperties'
|
|
|
|
String: 'programmableStringProperties'
|
2014-06-27 03:36:03 -04:00
|
|
|
Global: 'programmableGlobalProperties'
|
|
|
|
Function: 'programmableFunctionProperties'
|
|
|
|
RegExp: 'programmableRegExpProperties'
|
|
|
|
Date: 'programmableDateProperties'
|
|
|
|
Number: 'programmableNumberProperties'
|
|
|
|
JSON: 'programmableJSONProperties'
|
|
|
|
LoDash: 'programmableLoDashProperties'
|
2014-04-25 19:57:42 -04:00
|
|
|
Vector: 'programmableVectorProperties'
|
|
|
|
snippets: 'programmableSnippets'
|
|
|
|
else
|
|
|
|
propStorage =
|
2014-07-23 11:59:42 -04:00
|
|
|
'this': ['apiProperties', 'apiMethods']
|
2014-10-18 17:51:43 -04:00
|
|
|
if not (@options.level.get('type', true) in ['hero', 'hero-ladder', 'hero-coop']) or not @options.programmable
|
2014-10-17 00:38:11 -04:00
|
|
|
@organizePalette propStorage, allDocs, excludedDocs
|
|
|
|
else
|
|
|
|
@organizePaletteHero propStorage, allDocs, excludedDocs
|
|
|
|
|
|
|
|
organizePalette: (propStorage, allDocs, excludedDocs) ->
|
2014-02-24 18:48:30 -05:00
|
|
|
count = 0
|
|
|
|
propGroups = {}
|
2014-07-23 11:59:42 -04:00
|
|
|
for owner, storages of propStorage
|
|
|
|
storages = [storages] if _.isString storages
|
|
|
|
for storage in storages
|
|
|
|
props = _.reject @thang[storage] ? [], (prop) -> prop[0] is '_' # no private properties
|
2014-11-12 18:28:08 -05:00
|
|
|
props = _.uniq props
|
2014-07-23 11:59:42 -04:00
|
|
|
added = _.sortBy(props).slice()
|
|
|
|
propGroups[owner] = (propGroups[owner] ? []).concat added
|
|
|
|
count += added.length
|
2014-10-17 00:38:11 -04:00
|
|
|
Backbone.Mediator.publish 'tome:update-snippets', propGroups: propGroups, allDocs: allDocs, language: @options.language
|
2014-02-24 18:48:30 -05:00
|
|
|
|
|
|
|
shortenize = count > 6
|
|
|
|
tabbify = count >= 10
|
2014-02-12 19:42:09 -05:00
|
|
|
@entries = []
|
2014-02-24 18:48:30 -05:00
|
|
|
for owner, props of propGroups
|
2014-02-18 14:23:01 -05:00
|
|
|
for prop in props
|
2014-02-28 15:43:31 -05:00
|
|
|
doc = _.find (allDocs['__' + prop] ? []), (doc) ->
|
2014-02-24 18:48:30 -05:00
|
|
|
return true if doc.owner is owner
|
2014-02-24 20:53:35 -05:00
|
|
|
return (owner is 'this' or owner is 'more') and (not doc.owner? or doc.owner is 'this')
|
2014-07-15 22:25:53 -04:00
|
|
|
if not doc and not excludedDocs['__' + prop]
|
|
|
|
console.log 'could not find doc for', prop, 'from', allDocs['__' + prop], 'for', owner, 'of', propGroups
|
2014-07-15 21:54:55 -04:00
|
|
|
doc ?= prop
|
|
|
|
if doc
|
|
|
|
@entries.push @addEntry(doc, shortenize, tabbify, owner is 'snippets')
|
2014-02-24 18:48:30 -05:00
|
|
|
groupForEntry = (entry) ->
|
2014-04-25 19:57:42 -04:00
|
|
|
return 'more' if entry.doc.owner is 'this' and entry.doc.name in (propGroups.more ? [])
|
2014-02-24 18:48:30 -05:00
|
|
|
entry.doc.owner
|
2014-02-16 18:30:00 -05:00
|
|
|
@entries = _.sortBy @entries, (entry) ->
|
2014-06-25 23:19:11 -04:00
|
|
|
order = ['this', 'more', 'Math', 'Vector', 'String', 'Object', 'Array', 'Function', 'snippets']
|
2014-02-24 18:48:30 -05:00
|
|
|
index = order.indexOf groupForEntry entry
|
2014-02-16 18:30:00 -05:00
|
|
|
index = String.fromCharCode if index is -1 then order.length else index
|
|
|
|
index += entry.doc.name
|
|
|
|
if tabbify and _.find @entries, ((entry) -> entry.doc.owner isnt 'this')
|
2014-02-24 18:48:30 -05:00
|
|
|
@entryGroups = _.groupBy @entries, groupForEntry
|
2014-02-16 18:30:00 -05:00
|
|
|
else
|
2014-10-18 17:51:43 -04:00
|
|
|
i18nKey = if @options.level.get('type', true) in ['hero', 'hero-ladder', 'hero-coop'] then 'play_level.tome_your_skills' else 'play_level.tome_available_spells'
|
2014-09-24 18:48:18 -04:00
|
|
|
defaultGroup = $.i18n.t i18nKey
|
2014-02-16 18:30:00 -05:00
|
|
|
@entryGroups = {}
|
|
|
|
@entryGroups[defaultGroup] = @entries
|
2014-03-13 12:02:19 -04:00
|
|
|
@defaultGroupSlug = _.string.slugify defaultGroup
|
2014-02-16 18:30:00 -05:00
|
|
|
@entryGroupSlugs = {}
|
2014-06-25 23:19:11 -04:00
|
|
|
@entryGroupNames = {}
|
2014-02-16 18:30:00 -05:00
|
|
|
for group, entries of @entryGroups
|
|
|
|
@entryGroups[group] = _.groupBy entries, (entry, i) -> Math.floor i / N_ROWS
|
2014-06-25 23:19:11 -04:00
|
|
|
@entryGroupSlugs[group] = _.string.slugify group
|
|
|
|
@entryGroupNames[group] = group
|
|
|
|
if thisName = {coffeescript: '@', lua: 'self', clojure: 'self'}[@options.language]
|
|
|
|
if @entryGroupNames.this
|
|
|
|
@entryGroupNames.this = thisName
|
2014-02-16 18:30:00 -05:00
|
|
|
|
2014-10-17 00:38:11 -04:00
|
|
|
organizePaletteHero: (propStorage, allDocs, excludedDocs) ->
|
|
|
|
# Assign any kind of programmable properties to the items that grant them.
|
|
|
|
@isHero = true
|
|
|
|
itemThangTypes = {}
|
2014-11-05 18:43:08 -05:00
|
|
|
itemThangTypes[tt.get('name')] = tt for tt in @supermodel.getModels ThangType # Also heroes
|
2014-10-17 00:38:11 -04:00
|
|
|
propsByItem = {}
|
|
|
|
propCount = 0
|
|
|
|
itemsByProp = {}
|
2014-11-12 18:28:08 -05:00
|
|
|
# Make sure that we get the spellbook first, then the primary hand, then anything else.
|
|
|
|
slots = _.sortBy _.keys(@thang.inventoryThangTypeNames ? {}), (slot) ->
|
|
|
|
if slot is 'left-hand' then 0 else if slot is 'right-hand' then 1 else 2
|
|
|
|
for slot in slots
|
|
|
|
thangTypeName = @thang.inventoryThangTypeNames[slot]
|
2014-11-05 18:43:08 -05:00
|
|
|
if item = itemThangTypes[thangTypeName]
|
2014-11-01 12:35:19 -04:00
|
|
|
unless item.get('components')
|
|
|
|
console.error 'Item', item, 'did not have any components when we went to assemble docs.'
|
|
|
|
for component in item.get('components') ? [] when component.config
|
2014-10-17 00:38:11 -04:00
|
|
|
for owner, storages of propStorage
|
|
|
|
if props = component.config[storages]
|
2014-11-12 18:28:08 -05:00
|
|
|
for prop in _.sortBy(props) when prop[0] isnt '_' and not itemsByProp[prop] # no private properties
|
2014-10-17 00:38:11 -04:00
|
|
|
propsByItem[item.get('name')] ?= []
|
|
|
|
propsByItem[item.get('name')].push owner: owner, prop: prop, item: item
|
|
|
|
itemsByProp[prop] = item
|
|
|
|
++propCount
|
|
|
|
else
|
2014-11-05 18:43:08 -05:00
|
|
|
console.log @thang.id, "couldn't find item ThangType for", slot, thangTypeName
|
2014-10-17 00:38:11 -04:00
|
|
|
|
|
|
|
# Assign any unassigned properties to the hero itself.
|
|
|
|
for owner, storage of propStorage
|
|
|
|
for prop in _.reject(@thang[storage] ? [], (prop) -> itemsByProp[prop] or prop[0] is '_') # no private properties
|
2014-11-10 15:47:24 -05:00
|
|
|
if prop is 'say' and LevelOptions[@options.level.get('slug')]?.hidesSay # Hide for Dungeon Campaign
|
2014-11-05 18:43:08 -05:00
|
|
|
continue
|
2014-10-17 00:38:11 -04:00
|
|
|
propsByItem['Hero'] ?= []
|
2014-11-05 18:43:08 -05:00
|
|
|
propsByItem['Hero'].push owner: owner, prop: prop, item: itemThangTypes[@thang.spriteName]
|
2014-10-17 00:38:11 -04:00
|
|
|
++propCount
|
|
|
|
|
|
|
|
Backbone.Mediator.publish 'tome:update-snippets', propGroups: propsByItem, allDocs: allDocs, language: @options.language
|
|
|
|
|
|
|
|
shortenize = propCount > 6
|
|
|
|
@entries = []
|
|
|
|
for itemName, props of propsByItem
|
|
|
|
for prop, propIndex in props
|
|
|
|
item = prop.item
|
|
|
|
owner = prop.owner
|
|
|
|
prop = prop.prop
|
|
|
|
doc = _.find (allDocs['__' + prop] ? []), (doc) ->
|
|
|
|
return true if doc.owner is owner
|
|
|
|
return (owner is 'this' or owner is 'more') and (not doc.owner? or doc.owner is 'this')
|
|
|
|
if not doc and not excludedDocs['__' + prop]
|
2014-10-18 17:51:43 -04:00
|
|
|
console.log 'could not find doc for', prop, 'from', allDocs['__' + prop], 'for', owner, 'of', propsByItem, 'with item', item
|
2014-10-17 00:38:11 -04:00
|
|
|
doc ?= prop
|
|
|
|
if doc
|
|
|
|
@entries.push @addEntry(doc, shortenize, false, owner is 'snippets', item, propIndex > 0)
|
|
|
|
@entryGroups = _.groupBy @entries, (entry) -> itemsByProp[entry.doc.name]?.get('name') ? 'Hero'
|
|
|
|
iOSEntryGroups = {}
|
|
|
|
for group, entries of @entryGroups
|
2014-10-25 19:26:03 -04:00
|
|
|
iOSEntryGroups[group] =
|
|
|
|
item: {name: group, imageURL: itemThangTypes[group]?.getPortraitURL()}
|
|
|
|
props: (entry.doc for entry in entries)
|
2014-10-17 00:38:11 -04:00
|
|
|
Backbone.Mediator.publish 'tome:palette-updated', thangID: @thang.id, entryGroups: JSON.stringify(iOSEntryGroups)
|
|
|
|
|
|
|
|
addEntry: (doc, shortenize, tabbify, isSnippet=false, item=null, showImage=false) ->
|
2014-07-15 22:25:53 -04:00
|
|
|
writable = (if _.isString(doc) then doc else doc.name) in (@thang.apiUserProperties ? [])
|
2014-10-17 00:38:11 -04:00
|
|
|
new SpellPaletteEntryView doc: doc, thang: @thang, shortenize: shortenize, tabbify: tabbify, isSnippet: isSnippet, language: @options.language, writable: writable, level: @options.level, item: item, showImage: showImage
|
2014-01-03 13:32:13 -05:00
|
|
|
|
|
|
|
onDisableControls: (e) -> @toggleControls e, false
|
|
|
|
onEnableControls: (e) -> @toggleControls e, true
|
|
|
|
toggleControls: (e, enabled) ->
|
|
|
|
return if e.controls and not ('palette' in e.controls)
|
|
|
|
return if enabled is @controlsEnabled
|
|
|
|
@controlsEnabled = enabled
|
|
|
|
@$el.find('*').attr('disabled', not enabled)
|
2014-11-08 11:58:36 -05:00
|
|
|
@$el.toggleClass 'controls-disabled', not enabled
|
2014-01-03 13:32:13 -05:00
|
|
|
@toggleBackground()
|
|
|
|
|
|
|
|
toggleBackground: =>
|
|
|
|
# TODO: make the palette background an actual background and do the CSS trick
|
|
|
|
# used in spell_list_entry.sass for disabling
|
2014-07-23 08:38:12 -04:00
|
|
|
background = @$el.find('img.code-palette-background')[0]
|
2014-01-03 13:32:13 -05:00
|
|
|
if background.naturalWidth is 0 # not loaded yet
|
|
|
|
return _.delay @toggleBackground, 100
|
2014-07-23 08:38:12 -04:00
|
|
|
filters.revertImage background, 'span.code-palette-background' if @controlsEnabled
|
|
|
|
filters.darkenImage background, 'span.code-palette-background', 0.8 unless @controlsEnabled
|
2014-01-03 13:32:13 -05:00
|
|
|
|
|
|
|
onFrameChanged: (e) ->
|
|
|
|
return unless e.selectedThang?.id is @thang.id
|
2014-01-15 16:04:48 -05:00
|
|
|
@options.thang = @thang = e.selectedThang # Update our thang to the current version
|
2014-01-03 13:32:13 -05:00
|
|
|
|
2014-06-25 00:07:36 -04:00
|
|
|
onTomeChangedLanguage: (e) ->
|
|
|
|
@updateCodeLanguage e.language
|
2014-06-25 23:19:11 -04:00
|
|
|
entry.destroy() for entry in @entries
|
|
|
|
@createPalette()
|
|
|
|
@render()
|
2014-12-18 03:19:40 -05:00
|
|
|
|
|
|
|
onClickHelp: (e) ->
|
|
|
|
application.tracker?.trackEvent 'Spell palette help clicked', levelID: @level.get('slug')
|
|
|
|
@openModalView new GameMenuModal showTab: 'guide', level: @level, session: @session, supermodel: @supermodel
|
2014-06-25 00:07:36 -04:00
|
|
|
|
2014-01-03 13:32:13 -05:00
|
|
|
destroy: ->
|
|
|
|
entry.destroy() for entry in @entries
|
2014-02-12 15:41:41 -05:00
|
|
|
@toggleBackground = null
|
2014-10-17 00:38:11 -04:00
|
|
|
$(window).off 'resize', @onResize
|
2014-02-14 13:57:47 -05:00
|
|
|
super()
|