-
Notifications
You must be signed in to change notification settings - Fork 4.1k
/
PlayHeroesModal.coffee
392 lines (348 loc) · 16 KB
/
PlayHeroesModal.coffee
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
require('app/styles/play/modal/play-heroes-modal.sass')
ModalView = require 'views/core/ModalView'
template = require 'templates/play/modal/play-heroes-modal'
buyGemsPromptTemplate = require 'templates/play/modal/buy-gems-prompt'
CocoCollection = require 'collections/CocoCollection'
ThangType = require 'models/ThangType'
SpriteBuilder = require 'lib/sprites/SpriteBuilder'
AudioPlayer = require 'lib/AudioPlayer'
utils = require 'core/utils'
BuyGemsModal = require 'views/play/modal/BuyGemsModal'
CreateAccountModal = require 'views/core/CreateAccountModal'
SubscribeModal = require 'views/core/SubscribeModal'
Purchase = require 'models/Purchase'
LayerAdapter = require 'lib/surface/LayerAdapter'
Lank = require 'lib/surface/Lank'
store = require 'core/store'
createjs = require 'lib/createjs-parts'
module.exports = class PlayHeroesModal extends ModalView
className: 'modal fade play-modal'
template: template
id: 'play-heroes-modal'
events:
'slide.bs.carousel #hero-carousel': 'onHeroChanged'
'change #option-code-language': 'onCodeLanguageChanged'
'click #close-modal': 'hide'
'click #confirm-button': 'saveAndHide'
'click .unlock-button': 'onUnlockButtonClicked'
'click .subscribe-button': 'onSubscribeButtonClicked'
'click .buy-gems-prompt-button': 'onBuyGemsPromptButtonClicked'
'click': 'onClickedSomewhere'
shortcuts:
'left': -> @$el.find('#hero-carousel').carousel('prev') if @heroes.models.length and not @$el.hasClass 'secret'
'right': -> @$el.find('#hero-carousel').carousel('next') if @heroes.models.length and not @$el.hasClass 'secret'
'enter': -> @saveAndHide() if @visibleHero and not @visibleHero.locked
constructor: (options) ->
super options
options ?= {}
@confirmButtonI18N = options.confirmButtonI18N ? "common.save"
@heroes = new CocoCollection([], {model: ThangType})
@heroes.url = '/db/thang.type?view=heroes'
@heroes.setProjection ['original','name','slug','soundTriggers','featureImages','gems','heroClass','description','components','extendedName','shortName','unlockLevelName','i18n','poseImage','tier','releasePhase']
@heroes.comparator = 'gems'
@listenToOnce @heroes, 'sync', @onHeroesLoaded
@supermodel.loadCollection(@heroes, 'heroes')
@stages = {}
@layers = []
@session = options.session
@initCodeLanguageList options.hadEverChosenHero
@heroAnimationInterval = setInterval @animateHeroes, 1000
@trackTimeVisible()
onHeroesLoaded: ->
@formatHero hero for hero in @heroes.models
if me.freeOnly() or application.getHocCampaign()
@heroes.reset(@heroes.filter((hero) => !hero.locked))
unless me.isAdmin()
@heroes.reset(@heroes.filter((hero) => hero.get('releasePhase') isnt 'beta'))
formatHero: (hero) ->
hero.name = utils.i18n hero.attributes, 'extendedName'
hero.name ?= utils.i18n hero.attributes, 'shortName'
hero.name ?= utils.i18n hero.attributes, 'name'
hero.description = utils.i18n hero.attributes, 'description'
hero.unlockLevelName = utils.i18n hero.attributes, 'unlockLevelName'
original = hero.get('original')
hero.free = hero.attributes.slug in ['captain', 'knight', 'champion', 'duelist']
hero.unlockBySubscribing = hero.attributes.slug in ['samurai', 'ninja', 'librarian']
hero.premium = not hero.free and not hero.unlockBySubscribing
hero.locked = not me.ownsHero(original) and not (hero.unlockBySubscribing and me.isPremium())
hero.purchasable = hero.locked and me.isPremium()
if @options.level and allowedHeroes = @options.level.get 'allowedHeroes'
hero.restricted = not (hero.get('original') in allowedHeroes)
hero.class = (hero.get('heroClass') or 'warrior').toLowerCase()
hero.stats = hero.getHeroStats()
currentVisiblePremiumFeature: ->
isPremium = @visibleHero and not (@visibleHero.class is 'warrior' and @visibleHero.get('tier') is 0)
if isPremium
return {
viewName: @.id
featureName: 'view-hero'
premiumThang:
_id: @visibleHero.id
slug: @visibleHero.get('slug')
}
else
return null
getRenderData: (context={}) ->
context = super(context)
context.heroes = @heroes.models
context.level = @options.level
context.codeLanguages = @codeLanguageList
context.codeLanguage = @codeLanguage = @options?.session?.get('codeLanguage') ? me.get('aceConfig')?.language ? 'python'
context.confirmButtonI18N = @confirmButtonI18N
context.visibleHero = @visibleHero
context.gems = me.gems()
context.isIE = @isIE()
context
afterInsert: ->
@updateViewVisibleTimer()
super()
afterRender: ->
super()
return unless @supermodel.finished()
@playSound 'game-menu-open'
@$el.find('.hero-avatar').addClass 'ie' if @isIE()
heroes = @heroes.models
@$el.find('.hero-indicator').each ->
heroID = $(@).data('hero-id')
hero = _.find heroes, (hero) -> hero.get('original') is heroID
$(@).find('.hero-avatar').css('background-image', "url(#{hero.getPortraitURL()})").addClass('has-tooltip').tooltip()
@canvasWidth = 313 # @$el.find('canvas').width() # unreliable, whatever
@canvasHeight = @$el.find('canvas').height()
heroConfig = @options?.session?.get('heroConfig') ? me.get('heroConfig') ? {}
heroIndex = Math.max 0, _.findIndex(heroes, ((hero) -> hero.get('original') is heroConfig.thangType))
@$el.find(".hero-item:nth-child(#{heroIndex + 1}), .hero-indicator:nth-child(#{heroIndex + 1})").addClass('active')
@onHeroChanged direction: null, relatedTarget: @$el.find('.hero-item')[heroIndex]
@$el.find('.hero-stat').addClass('has-tooltip').tooltip()
@buildCodeLanguages()
rerenderFooter: ->
@formatHero @visibleHero
@renderSelectors '#hero-footer'
@buildCodeLanguages()
@$el.find('#gems-count-container').toggle Boolean @visibleHero.purchasable
initCodeLanguageList: (hadEverChosenHero) ->
if application.isIPadApp
@codeLanguageList = [
{id: 'python', name: "Python (#{$.i18n.t('choose_hero.default')})"}
{id: 'javascript', name: 'JavaScript'}
]
else
@codeLanguageList = [
{id: 'python', name: "Python (#{$.i18n.t('choose_hero.default')})"}
{id: 'javascript', name: 'JavaScript'}
{id: 'coffeescript', name: "CoffeeScript (#{$.i18n.t('choose_hero.experimental')})"}
]
if me.isAdmin() or not application.isProduction()
@codeLanguageList.push {id: 'java', name: "Java (#{$.i18n.t('choose_hero.experimental')})"}
@codeLanguageList.push {id: 'lua', name: "Lua (#{$.i18n.t('choose_hero.experimental')})"}
onHeroChanged: (e) ->
direction = e.direction # 'left' or 'right'
heroItem = $(e.relatedTarget)
hero = _.find @heroes.models, (hero) -> hero.get('original') is heroItem.data('hero-id')
return console.error "Couldn't find hero from heroItem:", heroItem unless hero
heroIndex = heroItem.index()
hero = @loadHero hero, heroIndex
@preloadHero heroIndex + 1
@preloadHero heroIndex - 1
@selectedHero = hero unless hero.locked
@visibleHero = hero
@rerenderFooter()
@trigger 'hero-loaded', {hero: hero}
@updateViewVisibleTimer()
getFullHero: (original) ->
url = "/db/thang.type/#{original}/version"
if fullHero = @supermodel.getModel url
return fullHero
fullHero = new ThangType()
fullHero.setURL url
fullHero = (@supermodel.loadModel fullHero).model
fullHero
preloadHero: (heroIndex) ->
return unless hero = @heroes.models[heroIndex]
@loadHero hero, heroIndex, true
loadHero: (hero, heroIndex, preloading=false) ->
createjs.Ticker.removeEventListener 'tick', stage for stage in _.values @stages
createjs.Ticker.framerate = 30 # In case we paused it from being inactive somewhere else
if poseImage = hero.get 'poseImage'
$(".hero-item[data-hero-id='#{hero.get('original')}'] canvas").hide()
$(".hero-item[data-hero-id='#{hero.get('original')}'] .hero-pose-image").show().find('img').prop('src', '/file/' + poseImage)
@playSelectionSound hero unless preloading
return hero
# TODO: remove animated hero code, as we have poseImages for all heroes.
if stage = @stages[heroIndex]
unless preloading
_.defer -> createjs.Ticker.addEventListener 'tick', stage # Deferred, otherwise it won't start updating for some reason.
@playSelectionSound hero
return hero
fullHero = @getFullHero hero.get 'original'
onLoaded = =>
canvas = $(".hero-item[data-hero-id='#{fullHero.get('original')}'] canvas")
return unless canvas.length # Don't render it if it's not on the screen.
unless fullHero.get 'raw'
console.error "Couldn't make animation for #{fullHero.get('name')} with attributes #{_.cloneDeep(fullHero.attributes)}. Was it loaded with an improper projection or something?", fullHero
@rerenderFooter()
return
canvas.show().prop width: @canvasWidth, height: @canvasHeight
layer = new LayerAdapter({webGL:true})
@layers.push layer
layer.resolutionFactor = 8 # hi res!
layer.buildAsync = false
multiplier = 7
layer.scaleX = layer.scaleY = multiplier
lank = new Lank(fullHero, {preloadSounds: false})
layer.addLank(lank)
layer.on 'new-spritesheet', ->
#- maybe put some more normalization here?
m = multiplier
m *= 0.75 if fullHero.get('slug') in ['knight', 'samurai', 'librarian', 'sorcerer', 'necromancer'] # These heroes are larger for some reason. Shrink 'em.
m *= 0.4 if fullHero.get('slug') is 'goliath' # Just too big!
m *= 0.9 if fullHero.get('slug') is 'champion' # Gotta fit her hair in there
layer.container.scaleX = layer.container.scaleY = m
layer.container.children[0].x = 160/m
layer.container.children[0].y = 250/m
if fullHero.get('slug') in ['forest-archer', 'librarian', 'sorcerer', 'potion-master', 'necromancer', 'code-ninja']
layer.container.children[0].y -= 3
if fullHero.get('slug') in ['librarian', 'sorcerer', 'potion-master', 'necromancer', 'goliath']
layer.container.children[0].x -= 3
stage = new createjs.SpriteStage(canvas[0])
@stages[heroIndex] = stage
stage.addChild layer.container
stage.update()
unless preloading
createjs.Ticker.addEventListener 'tick', stage
@playSelectionSound hero
@rerenderFooter()
if fullHero.loaded
_.defer onLoaded
else
@listenToOnce fullHero, 'sync', onLoaded
fullHero
animateHeroes: =>
return unless @visibleHero
heroIndex = Math.max 0, _.findIndex(@heroes.models, ((hero) => hero.get('original') is @visibleHero.get('original')))
animation = _.sample(['attack', 'move_side', 'move_fore']) # Must be in LayerAdapter default actions.
@stages[heroIndex]?.children?[0]?.children?[0]?.gotoAndPlay? animation
playSelectionSound: (hero) ->
return if @$el.hasClass 'secret'
@currentSoundInstance?.stop()
return unless soundTriggers = utils.i18n hero.attributes, 'soundTriggers'
return unless sounds = soundTriggers.selected
return unless sound = sounds[Math.floor Math.random() * sounds.length]
name = AudioPlayer.nameForSoundReference sound
AudioPlayer.preloadSoundReference sound
@currentSoundInstance = AudioPlayer.playSound name, 1
@currentSoundInstance
buildCodeLanguages: ->
$select = @$el.find('#option-code-language')
$select.fancySelect().parent().find('.options li').each ->
languageName = $(@).text()
languageID = $(@).data('value')
blurb = $.i18n.t("choose_hero.#{languageID}_blurb")
if languageName.indexOf(blurb) is -1 # Avoid doubling blurb if this is called 2x
$(@).text("#{languageName} - #{blurb}")
onCodeLanguageChanged: (e) ->
@codeLanguage = @$el.find('#option-code-language').val()
@codeLanguageChanged = true
window.tracker?.trackEvent 'Campaign changed code language', category: 'Campaign Hero Select', codeLanguage: @codeLanguage, levelSlug: @options.level?.get('slug')
#- Purchasing the hero
onUnlockButtonClicked: (e) ->
e.stopPropagation()
button = $(e.target).closest('button')
affordable = @visibleHero.get('gems') <= me.gems()
if not affordable
@playSound 'menu-button-click'
@askToBuyGems button unless me.freeOnly()
else if button.hasClass('confirm')
@playSound 'menu-button-unlock-end'
purchase = Purchase.makeFor(@visibleHero)
purchase.save()
#- set local changes to mimic what should happen on the server...
purchased = me.get('purchased') ? {}
purchased.heroes ?= []
purchased.heroes.push(@visibleHero.get('original'))
me.set('purchased', purchased)
me.set('spent', (me.get('spent') ? 0) + @visibleHero.get('gems'))
#- ...then rerender visible hero
heroEntry = @$el.find(".hero-item[data-hero-id='#{@visibleHero.get('original')}']")
heroEntry.find('.hero-status-value').attr('data-i18n', 'play.available').i18n()
@applyRTLIfNeeded()
heroEntry.removeClass 'locked purchasable'
@selectedHero = @visibleHero
@rerenderFooter()
Backbone.Mediator.publish 'store:hero-purchased', hero: @visibleHero, heroSlug: @visibleHero.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: ->
createAccountModal = new CreateAccountModal supermodel: @supermodel
return @openModalView createAccountModal
askToBuyGems: (unlockButton) ->
@$el.find('.unlock-button').popover 'destroy'
popoverTemplate = buyGemsPromptTemplate {}
unlockButton.popover(
animation: true
trigger: 'manual'
placement: 'left'
content: ' ' # template has it
container: @$el
template: popoverTemplate
).popover 'show'
popover = unlockButton.data('bs.popover')
popover?.$tip?.i18n()
@applyRTLIfNeeded()
onBuyGemsPromptButtonClicked: (e) ->
return @askToSignUp() if me.get('anonymous')
@openModalView new BuyGemsModal()
onClickedSomewhere: (e) ->
return if @destroyed
@$el.find('.unlock-button').popover 'destroy'
onSubscribeButtonClicked: (e) ->
@openModalView new SubscribeModal()
window.tracker?.trackEvent 'Show subscription modal', category: 'Subscription', label: 'hero subscribe modal: ' + ($(e.target).data('heroSlug') or 'unknown')
#- Exiting
saveAndHide: ->
hero = @selectedHero?.get('original')
hero ?= @visibleHero?.get('original') if @visibleHero?.loaded and not @visibleHero.locked
unless hero
console.error 'Somehow we tried to hide without having a hero selected yet...'
noty {
text: "Error: hero not loaded. If this keeps happening, please report the bug."
layout: 'topCenter'
timeout: 10000
type: 'error'
}
return
if @session
changed = @updateHeroConfig(@session, hero)
if @session.get('codeLanguage') isnt @codeLanguage
@session.set('codeLanguage', @codeLanguage)
changed = true
#Backbone.Mediator.publish 'tome:change-language', language: @codeLanguage, reload: true # We'll reload the PlayLevelView instead.
@session.patch() if changed
changed = @updateHeroConfig(me, hero)
aceConfig = _.clone(me.get('aceConfig')) or {}
if @codeLanguage isnt aceConfig.language
aceConfig.language = @codeLanguage
me.set 'aceConfig', aceConfig
changed = true
me.patch() if changed
@hide()
@trigger?('confirm-click', hero: @selectedHero)
updateHeroConfig: (model, hero) ->
return false unless hero
heroConfig = _.clone(model.get('heroConfig')) or {}
if heroConfig.thangType isnt hero
heroConfig.thangType = hero
model.set('heroConfig', heroConfig)
return true
onHidden: ->
super()
@playSound 'game-menu-close'
destroy: ->
clearInterval @heroAnimationInterval
for heroIndex, stage of @stages
createjs.Ticker.removeEventListener "tick", stage
stage.removeAllChildren()
layer.destroy() for layer in @layers
super()