Merge branch 'master' into production

This commit is contained in:
Nick Winter 2014-10-23 20:12:28 -07:00
commit f396114a47
19 changed files with 356 additions and 88 deletions

View file

@ -119,7 +119,7 @@ module.exports = class Angel extends CocoClass
return if @aborting return if @aborting
# Toggle BOX2D_ENABLED during deserialization so that if we have box2d in the namespace, the Collides Components still don't try to create bodies for deserialized Thangs upon attachment. # Toggle BOX2D_ENABLED during deserialization so that if we have box2d in the namespace, the Collides Components still don't try to create bodies for deserialized Thangs upon attachment.
window.BOX2D_ENABLED = false window.BOX2D_ENABLED = false
@streamingWorld = World.deserialize serialized, @shared.worldClassMap, @shared.lastSerializedWorldFrames, @finishBeholdingWorld(goalStates), startFrame, endFrame, streamingWorld @streamingWorld = World.deserialize serialized, @shared.worldClassMap, @shared.lastSerializedWorldFrames, @finishBeholdingWorld(goalStates), startFrame, endFrame, @work.level, streamingWorld
window.BOX2D_ENABLED = true window.BOX2D_ENABLED = true
@shared.lastSerializedWorldFrames = serialized.frames @shared.lastSerializedWorldFrames = serialized.frames
@ -253,7 +253,7 @@ module.exports = class Angel extends CocoClass
work.testWorld.goalManager.worldGenerationEnded() if work.testWorld.ended work.testWorld.goalManager.worldGenerationEnded() if work.testWorld.ended
serialized = testWorld.serialize() serialized = testWorld.serialize()
window.BOX2D_ENABLED = false window.BOX2D_ENABLED = false
World.deserialize serialized.serializedWorld, @shared.worldClassMap, @shared.lastSerializedWorldFrames, @finishBeholdingWorld(goalStates), serialized.startFrame, serialized.endFrame World.deserialize serialized.serializedWorld, @shared.worldClassMap, @shared.lastSerializedWorldFrames, @finishBeholdingWorld(goalStates), serialized.startFrame, work.level, serialized.endFrame
window.BOX2D_ENABLED = true window.BOX2D_ENABLED = true
@shared.lastSerializedWorldFrames = serialized.serializedWorld.frames @shared.lastSerializedWorldFrames = serialized.serializedWorld.frames

View file

@ -14,6 +14,8 @@ module.exports = class MusicPlayer extends CocoClass
'audio-player:loaded': 'onAudioLoaded' 'audio-player:loaded': 'onAudioLoaded'
'playback:real-time-playback-started': 'onRealTimePlaybackStarted' 'playback:real-time-playback-started': 'onRealTimePlaybackStarted'
'playback:real-time-playback-ended': 'onRealTimePlaybackEnded' 'playback:real-time-playback-ended': 'onRealTimePlaybackEnded'
'music-player:enter-menu': 'onEnterMenu'
'music-player:exit-menu': 'onExitMenu'
constructor: -> constructor: ->
super arguments... super arguments...
@ -35,9 +37,10 @@ module.exports = class MusicPlayer extends CocoClass
@standingBy = e @standingBy = e
return return
delay = e.delay ? 0
@standingBy = null @standingBy = null
@fadeOutCurrentMusic() @fadeOutCurrentMusic()
@startNewMusic(src) if e.play @startNewMusic(src, delay) if e.play
restartCurrentMusic: -> restartCurrentMusic: ->
return unless @currentMusic return unless @currentMusic
@ -46,15 +49,16 @@ module.exports = class MusicPlayer extends CocoClass
fadeOutCurrentMusic: -> fadeOutCurrentMusic: ->
return unless @currentMusic return unless @currentMusic
createjs.Tween.removeTweens(@currentMusic)
f = -> @stop() f = -> @stop()
createjs.Tween.get(@currentMusic).to({volume: 0.0}, CROSSFADE_LENGTH).call(f) createjs.Tween.get(@currentMusic).to({volume: 0.0}, CROSSFADE_LENGTH).call(f)
startNewMusic: (src) -> startNewMusic: (src, delay) ->
@currentMusic = createjs.Sound.play(src, 'none', 0, 0, -1, 0.3) if src @currentMusic = createjs.Sound.play(src, 'none', 0, 0, -1, 0.3) if src
return unless @currentMusic return unless @currentMusic
@currentMusic.volume = 0.0 @currentMusic.volume = 0.0
if me.get('music', true) if me.get('music', true)
createjs.Tween.get(@currentMusic).to({volume: MUSIC_VOLUME}, CROSSFADE_LENGTH) createjs.Tween.get(@currentMusic).wait(delay).to({volume: MUSIC_VOLUME}, CROSSFADE_LENGTH)
onMusicSettingChanged: -> onMusicSettingChanged: ->
@updateMusicVolume() @updateMusicVolume()
@ -74,6 +78,24 @@ module.exports = class MusicPlayer extends CocoClass
if @previousMusic if @previousMusic
@currentMusic = @previousMusic @currentMusic = @previousMusic
@restartCurrentMusic() @restartCurrentMusic()
if @currentMusic.volume
createjs.Tween.get(@currentMusic).wait(5000).to({volume: MUSIC_VOLUME}, CROSSFADE_LENGTH)
onEnterMenu: (e) ->
return if @inMenu
@inMenu = true
@previousMusic = @currentMusic
terrain = (e.terrain ? 'Dungeon').toLowerCase()
file = "/music/music-menu-#{terrain}"
Backbone.Mediator.publish 'music-player:play-music', file: file, play: true, delay: 1000
onExitMenu: (e) ->
return unless @inMenu
@inMenu = false
@fadeOutCurrentMusic()
if @previousMusic
@currentMusic = @previousMusic
@restartCurrentMusic()
destroy: -> destroy: ->
me.off 'change:music', @onMusicSettingChanged, @ me.off 'change:music', @onMusicSettingChanged, @

View file

@ -151,10 +151,14 @@ module.exports = class Thang
o.unusedTrackedPropertyKeys = (@trackedPropertiesKeys[propIndex] for used, propIndex in @trackedPropertiesUsed when not used) o.unusedTrackedPropertyKeys = (@trackedPropertiesKeys[propIndex] for used, propIndex in @trackedPropertiesUsed when not used)
o o
@deserialize: (o, world, classMap) -> @deserialize: (o, world, classMap, levelComponents) ->
t = new Thang world, o.spriteName, o.id t = new Thang world, o.spriteName, o.id
for [componentClassName, componentConfig] in o.components for [componentClassName, componentConfig] in o.components
componentClass = classMap[componentClassName] unless componentClass = classMap[componentClassName]
console.debug 'Compiling new Component while deserializing:', componentClassName
componentModel = _.find levelComponents, name: componentClassName
componentClass = world.loadClassFromCode componentModel.js, componentClassName, 'component'
world.classMap[componentClassName] = componentClass
t.addComponents [componentClass, componentConfig] t.addComponents [componentClass, componentConfig]
t.unusedTrackedPropertyKeys = o.unusedTrackedPropertyKeys t.unusedTrackedPropertyKeys = o.unusedTrackedPropertyKeys
t.unusedTrackedPropertyValues = (t[prop] for prop in o.unusedTrackedPropertyKeys) t.unusedTrackedPropertyValues = (t[prop] for prop in o.unusedTrackedPropertyKeys)

View file

@ -436,7 +436,7 @@ module.exports = class World
console.log 'Whoa, serializing a lot of WorldScriptNotes here:', o.scriptNotes.length console.log 'Whoa, serializing a lot of WorldScriptNotes here:', o.scriptNotes.length
{serializedWorld: o, transferableObjects: [o.storageBuffer], startFrame: startFrame, endFrame: endFrame} {serializedWorld: o, transferableObjects: [o.storageBuffer], startFrame: startFrame, endFrame: endFrame}
@deserialize: (o, classMap, oldSerializedWorldFrames, finishedWorldCallback, startFrame, endFrame, streamingWorld) -> @deserialize: (o, classMap, oldSerializedWorldFrames, finishedWorldCallback, startFrame, endFrame, level, streamingWorld) ->
# Code hotspot; optimize it # Code hotspot; optimize it
#console.log 'Deserializing', o, 'length', JSON.stringify(o).length #console.log 'Deserializing', o, 'length', JSON.stringify(o).length
#console.log JSON.stringify(o) #console.log JSON.stringify(o)
@ -461,10 +461,10 @@ module.exports = class World
perf.t1 = now() perf.t1 = now()
if w.thangs.length if w.thangs.length
for thangConfig in o.thangs when not w.thangMap[thangConfig.id] for thangConfig in o.thangs when not w.thangMap[thangConfig.id]
w.thangs.push thang = Thang.deserialize(thangConfig, w, classMap) w.thangs.push thang = Thang.deserialize(thangConfig, w, classMap, level.levelComponents)
w.setThang thang w.setThang thang
else else
w.thangs = (Thang.deserialize(thang, w, classMap) for thang in o.thangs) w.thangs = (Thang.deserialize(thang, w, classMap, level.levelComponents) for thang in o.thangs)
w.setThang thang for thang in w.thangs w.setThang thang for thang in w.thangs
w.scriptNotes = (WorldScriptNote.deserialize(sn, w, classMap) for sn in o.scriptNotes) w.scriptNotes = (WorldScriptNote.deserialize(sn, w, classMap) for sn in o.scriptNotes)
perf.t2 = now() perf.t2 = now()

View file

@ -202,8 +202,13 @@
victory_sign_up_poke: "Want to save your code? Create a free account!" victory_sign_up_poke: "Want to save your code? Create a free account!"
victory_rate_the_level: "Rate the level: " # Only in old-style levels. victory_rate_the_level: "Rate the level: " # Only in old-style levels.
victory_return_to_ladder: "Return to Ladder" victory_return_to_ladder: "Return to Ladder"
victory_play_next_level: "Play Next Level" # Only in old-style levels.
victory_play_continue: "Continue" victory_play_continue: "Continue"
victory_play_skip: "Skip Ahead"
victory_play_next_level: "Play Next Level"
victory_play_more_practice: "More Practice"
victory_play_too_easy: "Too Easy"
victory_play_just_right: "Just Right"
victory_play_too_hard: "Too Hard"
victory_saving_progress: "Saving Progress" victory_saving_progress: "Saving Progress"
victory_go_home: "Go Home" # Only in old-style levels. victory_go_home: "Go Home" # Only in old-style levels.
victory_review: "Tell us more!" # Only in old-style levels. victory_review: "Tell us more!" # Only in old-style levels.

View file

@ -61,10 +61,10 @@ module.exports = nativeDescription: "русский", englishDescription: "Russi
achievements: "Достижения" # Tooltip on achievement list button from /play achievements: "Достижения" # Tooltip on achievement list button from /play
account: "Аккаунт" # Tooltip on account button from /play account: "Аккаунт" # Tooltip on account button from /play
settings: "Настройки" # Tooltip on settings button from /play settings: "Настройки" # Tooltip on settings button from /play
next: "Следующий" # Go from choose hero to choose inventory before playing a level next: "Выбрать" # Go from choose hero to choose inventory before playing a level
change_hero: "Выбрать героя" # Go back from choose inventory to choose hero change_hero: "Выбрать героя" # Go back from choose inventory to choose hero
choose_inventory: "Выбрать предметы" choose_inventory: "Выбрать предметы"
older_campaigns: "Старые компании" older_campaigns: "Старые кампании"
anonymous: "Неизвестный игрок" anonymous: "Неизвестный игрок"
level_difficulty: "Сложность: " level_difficulty: "Сложность: "
campaign_beginner: "Кампания для новичков" campaign_beginner: "Кампания для новичков"
@ -72,7 +72,7 @@ module.exports = nativeDescription: "русский", englishDescription: "Russi
adventurer_prefix: "Вы можете зайти на любой из этих уровней, а также обсудить уровни на " adventurer_prefix: "Вы можете зайти на любой из этих уровней, а также обсудить уровни на "
adventurer_forum: "форуме Искателей приключений" adventurer_forum: "форуме Искателей приключений"
adventurer_suffix: "." adventurer_suffix: "."
campaign_old_beginner: "Старые компании для новичков" campaign_old_beginner: "Старые кампании для новичков"
campaign_old_beginner_description: "... в которой вы познакомитесь с магией программирования." campaign_old_beginner_description: "... в которой вы познакомитесь с магией программирования."
campaign_dev: "Случайные уровни потруднее" campaign_dev: "Случайные уровни потруднее"
campaign_dev_description: "... в которых вы изучите интерфейс и научитесь делать кое-что посложнее." campaign_dev_description: "... в которых вы изучите интерфейс и научитесь делать кое-что посложнее."
@ -227,7 +227,7 @@ module.exports = nativeDescription: "русский", englishDescription: "Russi
tome_available_spells: "Доступные заклинания" tome_available_spells: "Доступные заклинания"
tome_your_skills: "Ваши навыки" tome_your_skills: "Ваши навыки"
hud_continue: "Продолжить (Shift+Пробел)" hud_continue: "Продолжить (Shift+Пробел)"
# code_saved: "Code Saved" code_saved: "Код сохранен"
skip_tutorial: "Пропуск (Esc)" skip_tutorial: "Пропуск (Esc)"
keyboard_shortcuts: "Горячие клавиши" keyboard_shortcuts: "Горячие клавиши"
loading_ready: "Готово!" loading_ready: "Готово!"
@ -279,12 +279,12 @@ module.exports = nativeDescription: "русский", englishDescription: "Russi
inventory_caption: "Оденьте своего героя" inventory_caption: "Оденьте своего героя"
choose_hero_caption: "Выбор героя, языка" choose_hero_caption: "Выбор героя, языка"
# save_load_caption: "... and view history" # save_load_caption: "... and view history"
# options_caption: "Configure settings" options_caption: "Выбор настроек"
# guide_caption: "Docs and tips" # guide_caption: "Docs and tips"
multiplayer_caption: "Играй с друзьями!" multiplayer_caption: "Играй с друзьями!"
# inventory: inventory:
# choose_inventory: "Equip Items" choose_inventory: "Выбрать предметы"
choose_hero: choose_hero:
choose_hero: "Выберите героя" choose_hero: "Выберите героя"
@ -333,10 +333,10 @@ module.exports = nativeDescription: "русский", englishDescription: "Russi
why_paragraph_2_italic_caps: "НЕТ, МАМ, Я ДОЛЖЕН ПРОЙТИ УРОВЕНЬ!" why_paragraph_2_italic_caps: "НЕТ, МАМ, Я ДОЛЖЕН ПРОЙТИ УРОВЕНЬ!"
why_paragraph_2_suffix: "Вот, почему CodeCombat - мультиплеерная игра, а не курс уроков в игровой форме. Мы не остановимся, пока вы не потеряете голову - в данном случае, это хорошо." why_paragraph_2_suffix: "Вот, почему CodeCombat - мультиплеерная игра, а не курс уроков в игровой форме. Мы не остановимся, пока вы не потеряете голову - в данном случае, это хорошо."
why_paragraph_3: "Если вы собираетесь увлечься какой-нибудь игрой, увлекитесь этой и станьте одним из волшебников века информационных технологий." why_paragraph_3: "Если вы собираетесь увлечься какой-нибудь игрой, увлекитесь этой и станьте одним из волшебников века информационных технологий."
# press_title: "Bloggers/Press" press_title: "Блогерам/Прессе"
# press_paragraph_1_prefix: "Want to write about us? Feel free to download and use all of the resources included in our" press_paragraph_1_prefix: "Хотите написать о нас? Скачивайте и используйте все ресурсы, включенный в наш"
# press_paragraph_1_link: "press packet" press_paragraph_1_link: "пресс-пакет"
# press_paragraph_1_suffix: ". All logos and images may be used without contacting us directly." press_paragraph_1_suffix: ". Все изображения могут быть использованы без предварительного уведомления."
# team: "Team" # team: "Team"
# george_title: "CEO" # george_title: "CEO"
# george_blurb: "Businesser" # george_blurb: "Businesser"
@ -494,7 +494,7 @@ module.exports = nativeDescription: "русский", englishDescription: "Russi
level_tab_thangs_add: "Добавить объект" level_tab_thangs_add: "Добавить объект"
delete: "Удалить" delete: "Удалить"
duplicate: "Дублировать" duplicate: "Дублировать"
# rotate: "Rotate" rotate: "Повернуть"
level_settings_title: "Настройки" level_settings_title: "Настройки"
level_component_tab_title: "Текущие компоненты" level_component_tab_title: "Текущие компоненты"
level_component_btn_new: "Создать новый компонент" level_component_btn_new: "Создать новый компонент"

View file

@ -76,3 +76,15 @@ module.exports = class User extends CocoModel
earnedHero: (heroOriginal) -> heroOriginal in (me.get('earned')?.heroes ? []) earnedHero: (heroOriginal) -> heroOriginal in (me.get('earned')?.heroes ? [])
earnedItem: (itemOriginal) -> itemOriginal in (me.get('earned')?.items ? []) earnedItem: (itemOriginal) -> itemOriginal in (me.get('earned')?.items ? [])
earnedLevel: (levelOriginal) -> levelOriginal in (me.get('earned')?.levels ? []) earnedLevel: (levelOriginal) -> levelOriginal in (me.get('earned')?.levels ? [])
getBranchingGroup: ->
return @branchingGroup if @branchingGroup
group = me.get('testGroupNumber') % 4
@branchingGroup = switch group
when 0 then 'no-practice'
when 1 then 'all-practice'
when 2 then 'choice-explicit'
when 3 then 'choice-implicit'
@branchingGroup = 'choice-explicit' if me.isAdmin()
application.tracker.identify branchingGroup: @branchingGroup
@branchingGroup

View file

@ -18,6 +18,12 @@ module.exports =
'music-player:play-music': c.object {required: ['play']}, 'music-player:play-music': c.object {required: ['play']},
play: {type: 'boolean'} play: {type: 'boolean'}
file: {type: 'string'} file: {type: 'string'}
delay: {type: 'integer', minimum: 0, format: 'milliseconds'}
'music-player:enter-menu': c.object {required: []},
terrain: {type: 'string'}
'music-player:exit-menu': c.object {}
'modal:opened': c.object {} 'modal:opened': c.object {}

View file

@ -168,43 +168,49 @@
//- Footer //- Footer
.modal-content
padding-bottom: 50px // so the footer appears at the bottom
&.with-sign-up .modal-content
padding-bottom: 100px // need more space for signup poke
.modal-footer .modal-footer
position: absolute padding-bottom: 0
bottom: -20px
left: 20px
right: 20px
#totals
color: white
p.sign-up-poke p.sign-up-poke
position: absolute
bottom: 60px
right: 20px
color: white color: white
.sign-up-button .sign-up-button
float: right float: right
margin-left: 10px margin: 2px 10px
.ladder-submission-view #totals
display: inline-block
color: white color: white
.rank-button.btn-block .next-level-buttons
display: inline-block float: right
width: initial
padding-left: 19px
padding-right: 19px
.last-submitted .next-level-button
float: none display: block
margin: 8px 10px
width: 150px
.ladder-submission-view
display: inline-block
color: white
.rank-button.btn-block
display: inline-block
width: initial
padding-left: 19px
padding-right: 19px
.last-submitted
float: none
.next-levels-prompt
display: none
margin: 30px -21px
.btn
width: 30%
width: -webkit-calc(33.333333% - 10px)
width: calc(33.333333% - 10px)
margin: 5px
html.no-borderimage html.no-borderimage

View file

@ -41,8 +41,17 @@ block modal-body-content
img(src=item.getPortraitURL()) img(src=item.getPortraitURL())
.reward-text= animate ? 'New Item' : item.get('name') .reward-text= animate ? 'New Item' : item.get('name')
.next-levels-prompt
for button in continueButtons
- var enabled = Boolean(button.link != '/play' || me.getBranchingGroup() == 'choice-implicit' || button.key == 'continue');
a.btn.btn-success.btn-lg.world-map-button.next-level-branch-button(href=button.link, disabled=!enabled, data-dismiss="modal", data-i18n="play_level.victory_play_" + button[me.getBranchingGroup()], data-branch-key=button.key)
block modal-footer-content block modal-footer-content
if me.get('anonymous')
p.sign-up-poke.hide
button.btn.btn-success.sign-up-button.btn-large(data-toggle="coco-modal", data-target="modal/SignupModal", data-i18n="play_level.victory_sign_up") Sign Up to Save Progress
span(data-i18n="play_level.victory_sign_up_poke") Want to save your code? Create a free account!
div#totals.pull-left div#totals.pull-left
span.spr Experience Gained: span.spr Experience Gained:
span#xp-total +0 span#xp-total +0
@ -52,15 +61,10 @@ block modal-footer-content
button.btn.btn-warning.hide#saving-progress-label(disabled, data-i18n="play_level.victory_saving_progress") Saving Progress button.btn.btn-warning.hide#saving-progress-label(disabled, data-i18n="play_level.victory_saving_progress") Saving Progress
if readyToRank .next-level-buttons
.ladder-submission-view if readyToRank
else if level.get('type') === 'hero-ladder' .ladder-submission-view
a.btn.btn-primary(href="/play/ladder/#{level.get('slug')}#my-matches", data-dismiss="modal", data-i18n="play_level.victory_return_to_ladder") Return to Ladder else if level.get('type') === 'hero-ladder'
else a.btn.btn-primary(href="/play/ladder/#{level.get('slug')}#my-matches", data-dismiss="modal", data-i18n="play_level.victory_return_to_ladder") Return to Ladder
a.btn.btn-success.world-map-button.hide#continue-button(href="/play-hero", data-dismiss="modal", data-i18n="play_level.victory_play_continue") Continue else
button.btn.btn-success.world-map-button.next-level-button.hide#continue-button(data-i18n="play_level.victory_play_continue") Continue
if me.get('anonymous')
p.sign-up-poke.hide
button.btn.btn-success.sign-up-button.btn-large(data-toggle="coco-modal", data-target="modal/SignupModal", data-i18n="play_level.victory_sign_up") Sign Up to Save Progress
span(data-i18n="play_level.victory_sign_up_poke") Want to save your code? Create a free account!

View file

@ -5,12 +5,14 @@
.gradient.vertical-gradient.left-gradient .gradient.vertical-gradient.left-gradient
img.map-background(src="/images/pages/play/map_" + mapType + ".jpg", alt="") img.map-background(src="/images/pages/play/map_" + mapType + ".jpg", alt="")
- var seenNext = false; - var seenNext = nextLevel;
each campaign in campaigns each campaign in campaigns
each level in campaign.levels each level in campaign.levels
- var next = !seenNext && levelStatusMap[level.id] != "complete"; if level.hidden
continue;
- var next = level.id == nextLevel || (!seenNext && levelStatusMap[level.id] != "complete" && !level.locked && !level.disabled);
- seenNext = seenNext || next; - seenNext = seenNext || next;
div(style="left: #{level.x}%; bottom: #{level.y}%; background-color: #{campaign.color}", class="level" + (next ? " next" : "") + (level.disabled ? " disabled" : "") + (level.locked ? " locked" : "") + " " + levelStatusMap[level.id] || "", data-level-id=level.id, title=level.name) div(style="left: #{level.x}%; bottom: #{level.y}%; background-color: #{level.color}", class="level" + (next ? " next" : "") + (level.disabled ? " disabled" : "") + (level.locked ? " locked" : "") + " " + levelStatusMap[level.id] || "", data-level-id=level.id, title=level.name)
a(href=level.type == 'hero' ? '#' : level.disabled ? "/play" : "/play/#{level.levelPath || 'level'}/#{level.id}", disabled=level.disabled, data-level-id=level.id, data-level-path=level.levelPath || 'level', data-level-name=level.name) a(href=level.type == 'hero' ? '#' : level.disabled ? "/play" : "/play/#{level.levelPath || 'level'}/#{level.id}", disabled=level.disabled, data-level-id=level.id, data-level-path=level.levelPath || 'level', data-level-name=level.name)
div(style="left: #{level.x}%; bottom: #{level.y}%", class="level-shadow" + (next ? " next" : "") + " " + levelStatusMap[level.id] || "") div(style="left: #{level.x}%; bottom: #{level.y}%", class="level-shadow" + (next ? " next" : "") + " " + levelStatusMap[level.id] || "")
.level-info-container(data-level-id=level.id, data-level-path=level.levelPath || 'level', data-level-name=level.name) .level-info-container(data-level-id=level.id, data-level-path=level.levelPath || 'level', data-level-name=level.name)

View file

@ -501,7 +501,7 @@ module.exports = class ThangsTabView extends CocoView
# foldered thangs -> array of thangs # foldered thangs -> array of thangs
flattened = [] flattened = []
for key, value of thangs for key, value of thangs
if value.id and value.thangType if value.id? and value.thangType
flattened.push value flattened.push value
else else
flattened = flattened.concat @flattenThangs(value) flattened = flattened.concat @flattenThangs(value)
@ -675,10 +675,10 @@ class ThangsFolderNode extends TreemaNode.nodeMap.object
valEl.append(el) valEl.append(el)
countThangs: (data) -> countThangs: (data) ->
return 0 if data.thangType and data.id return 0 if data.thangType and data.id?
num = 0 num = 0
for key, value of data for key, value of data
if value.thangType and value.id if value.thangType and value.id?
num += 1 num += 1
else else
num += @countThangs(value) num += @countThangs(value)

View file

@ -24,6 +24,7 @@ module.exports = class GameMenuModal extends ModalView
@options.showInventory = @options.level.get('type', true) in ['hero', 'hero-ladder', 'hero-coop'] @options.showInventory = @options.level.get('type', true) in ['hero', 'hero-ladder', 'hero-coop']
@options.levelID = @options.level.get('slug') @options.levelID = @options.level.get('slug')
@options.startingSessionHeroConfig = $.extend {}, true, (@options.session.get('heroConfig') ? {}) @options.startingSessionHeroConfig = $.extend {}, true, (@options.session.get('heroConfig') ? {})
Backbone.Mediator.publish 'music-player:enter-menu', terrain: @options.level.get('terrain', true)
getRenderData: (context={}) -> getRenderData: (context={}) ->
context = super(context) context = super(context)
@ -50,6 +51,7 @@ module.exports = class GameMenuModal extends ModalView
patchingMe = @updateConfig() patchingMe = @updateConfig()
me.patch() unless patchingMe # Might need to patch for options menu, too me.patch() unless patchingMe # Might need to patch for options menu, too
Backbone.Mediator.publish 'audio-player:play-sound', trigger: 'game-menu-close', volume: 1 Backbone.Mediator.publish 'audio-player:play-sound', trigger: 'game-menu-close', volume: 1
Backbone.Mediator.publish 'music-player:exit-menu', {}
updateConfig: -> updateConfig: ->
sessionHeroConfig = @options.startingSessionHeroConfig sessionHeroConfig = @options.startingSessionHeroConfig

View file

@ -32,6 +32,7 @@ module.exports = class WorldMapView extends RootView
constructor: (options) -> constructor: (options) ->
super options super options
@nextLevel = @getQueryVariable 'next'
@levelStatusMap = {} @levelStatusMap = {}
@levelPlayCountMap = {} @levelPlayCountMap = {}
@sessions = @supermodel.loadCollection(new LevelSessionsCollection(), 'your_sessions', null, 0).model @sessions = @supermodel.loadCollection(new LevelSessionsCollection(), 'your_sessions', null, 0).model
@ -83,11 +84,17 @@ module.exports = class WorldMapView extends RootView
level.x ?= 10 + 80 * Math.random() level.x ?= 10 + 80 * Math.random()
level.y ?= 10 + 80 * Math.random() level.y ?= 10 + 80 * Math.random()
level.locked = index > 0 and not me.earnedLevel level.original level.locked = index > 0 and not me.earnedLevel level.original
level.locked = false if window.levelUnlocksNotWorking window.levelUnlocksNotWorking = true if level.locked and level.id is @nextLevel # Temporary
level.locked = false if window.levelUnlocksNotWorking # Temporary; also possible in HeroVictoryModal
level.color = campaign.color
if level.practice
level.color = 'rgb(80, 130, 200)' unless me.getBranchingGroup() is 'all-practice'
level.hidden = true if me.getBranchingGroup() is 'no-practice'
context.levelStatusMap = @levelStatusMap context.levelStatusMap = @levelStatusMap
context.levelPlayCountMap = @levelPlayCountMap context.levelPlayCountMap = @levelPlayCountMap
context.isIPadApp = application.isIPadApp context.isIPadApp = application.isIPadApp
context.mapType = _.string.slugify @terrain context.mapType = _.string.slugify @terrain
context.nextLevel = @nextLevel
context context
afterRender: -> afterRender: ->
@ -599,6 +606,9 @@ hero = [
description: 'Grab the gem, but touch nothing else. Start here.' description: 'Grab the gem, but touch nothing else. Start here.'
x: 14 x: 14
y: 15.5 y: 15.5
nextLevels:
continue: 'gems-in-the-deep'
skip_ahead: 'shadow-guard'
} }
{ {
name: 'Gems in the Deep' name: 'Gems in the Deep'
@ -609,6 +619,24 @@ hero = [
description: 'Quickly collect the gems; you will need them.' description: 'Quickly collect the gems; you will need them.'
x: 32 x: 32
y: 15.5 y: 15.5
nextLevels:
more_practice: 'gem-grabber'
continue: 'shadow-guard'
skip_ahead: 'true-names'
}
{
name: 'Gem Grabber'
type: 'hero'
difficulty: 1
id: 'gem-grabber'
original: '54174347844506ae0195a0b8'
description: 'Grab even more gems as you practice moving.'
x: 35.49
y: 24.61
nextLevels:
continue: 'shadow-guard'
practice: true
disabled: true
} }
{ {
name: 'Shadow Guard' name: 'Shadow Guard'
@ -619,6 +647,23 @@ hero = [
description: 'Evade the Kithgard minion.' description: 'Evade the Kithgard minion.'
x: 54 x: 54
y: 9 y: 9
nextLevels:
more_practice: 'munchkin-dodger'
continue: 'true-names'
}
{
name: 'Munchkin Dodger'
type: 'hero'
difficulty: 1
id: 'munchkin-dodger'
original: '541875da4c16460000ab990f'
description: 'Practice your evasion skills with more guards.'
x: 61.19
y: 13.80
nextLevels:
continue: 'true-names'
practice: true
disabled: true
} }
{ {
name: 'True Names' name: 'True Names'
@ -629,6 +674,23 @@ hero = [
description: 'Learn an enemy\'s true name to defeat it.' description: 'Learn an enemy\'s true name to defeat it.'
x: 74 x: 74
y: 12 y: 12
nextLevels:
more_practice: 'munchkin-slayer'
continue: 'the-raised-sword'
}
{
name: 'Munchkin Slayer'
type: 'hero'
difficulty: 1
id: 'munchkin-slayer'
original: '5418aec24c16460000ab9aa6'
description: 'Test out your battle skills by defeating more munchkins.'
x: 80.85
y: 11.85
nextLevels:
continue: 'the-raised-sword'
practice: true
disabled: true
} }
{ {
name: 'The Raised Sword' name: 'The Raised Sword'
@ -639,6 +701,8 @@ hero = [
description: 'Learn to equip yourself for combat.' description: 'Learn to equip yourself for combat.'
x: 85 x: 85
y: 20 y: 20
nextLevels:
continue: 'the-first-kithmaze'
} }
{ {
name: 'The First Kithmaze' name: 'The First Kithmaze'
@ -649,6 +713,24 @@ hero = [
description: 'The builders of Kith constructed many mazes to confuse travelers.' description: 'The builders of Kith constructed many mazes to confuse travelers.'
x: 70 x: 70
y: 28 y: 28
nextLevels:
more_practice: 'the-one-point-fifth-kithmaze'
continue: 'the-second-kithmaze'
skip_ahead: 'new-sight'
}
{
name: 'The One-Point-Fifth Kithmaze'
type: 'hero'
difficulty: 1
id: 'the-one-point-fifth-kithmaze'
original: '5418cf256bae62f707c7e1c3'
description: 'Another day, another maze.'
x: 78.47
y: 34.38
nextLevels:
continue: 'the-second-kithmaze'
practice: true
disabled: true
} }
{ {
name: 'The Second Kithmaze' name: 'The Second Kithmaze'
@ -659,6 +741,23 @@ hero = [
description: 'Many have tried, few have found their way through this maze.' description: 'Many have tried, few have found their way through this maze.'
x: 55.54 x: 55.54
y: 26.96 y: 26.96
nextLevels:
more_practice: 'the-two-point-fifth-kithmaze'
continue: 'new-sight'
}
{
name: 'The Two-Point-Fifth Kithmaze'
type: 'hero'
difficulty: 1
id: 'the-two-point-fifth-kithmaze'
original: '5418d40f4c16460000ab9ac2'
description: 'You must really like doing these mazes!'
x: 49.02
y: 25.78
nextLevels:
continue: 'new-sight'
practice: true
disabled: true
} }
{ {
name: 'New Sight' name: 'New Sight'
@ -669,6 +768,8 @@ hero = [
description: 'A true name can only be seen with the correct lenses.' description: 'A true name can only be seen with the correct lenses.'
x: 67 x: 67
y: 41 y: 41
nextLevels:
continue: 'lowly-kithmen'
} }
{ {
name: 'Lowly Kithmen' name: 'Lowly Kithmen'
@ -679,6 +780,24 @@ hero = [
description: 'Use your glasses to seek out and attack the Kithmen.' description: 'Use your glasses to seek out and attack the Kithmen.'
x: 74 x: 74
y: 48 y: 48
nextLevels:
more_practice: 'still-pretty-low-kithmen'
continue: 'closing-the-distance'
skip_ahead: 'the-final-kithmaze'
}
{
name: 'Still-Pretty-Low Kithmen'
type: 'hero'
difficulty: 1
id: 'still-pretty-low-kithmen'
original: '541b288e1ccc8eaae19f3c25'
description: 'Now that you can see them, they\'re everywhere!'
x: 80.17
y: 45.31
nextLevels:
continue: 'closing-the-distance'
practice: true
disabled: true
} }
{ {
name: 'Closing the Distance' name: 'Closing the Distance'
@ -689,6 +808,23 @@ hero = [
description: 'Kithmen are not the only ones to stand in your way.' description: 'Kithmen are not the only ones to stand in your way.'
x: 76 x: 76
y: 60 y: 60
nextLevels:
more_practice: 'assassinating-more-kithmen'
continue: 'the-final-kithmaze'
}
{
name: 'Assassinating More Kithmen'
type: 'hero'
difficulty: 1
id: 'assassinating-more-kithmen'
original: '541b434e1ccc8eaae19f3c33'
description: 'They\'re, uh, coming right for us?'
x: 80.34
y: 55.60
nextLevels:
continue: 'the-final-kithmaze'
practice: true
disabled: true
} }
{ {
name: 'The Final Kithmaze' name: 'The Final Kithmaze'
@ -699,6 +835,23 @@ hero = [
description: 'To escape you must find your way through an Elder Kithman\'s maze.' description: 'To escape you must find your way through an Elder Kithman\'s maze.'
x: 82 x: 82
y: 70 y: 70
nextLevels:
more_practice: 'okay-one-more-kithmaze'
continue: 'kithgard-gates'
}
{
name: 'Okay, One More Kithmaze'
type: 'hero'
difficulty: 1
id: 'okay-one-more-kithmaze'
original: '541c9a30c6362edfb0f34479'
description: 'But you really gotta get outside after this, okay?'
x: 76.94
y: 74.22
nextLevels:
continue: 'kithgard-gates'
practice: true
disabled: true
} }
{ {
name: 'Kithgard Gates' name: 'Kithgard Gates'
@ -707,9 +860,10 @@ hero = [
id: 'kithgard-gates' id: 'kithgard-gates'
original: '541c9a30c6362edfb0f34479' original: '541c9a30c6362edfb0f34479'
description: 'Escape the Kithgard dungeons and don\'t let the guardians get you.' description: 'Escape the Kithgard dungeons and don\'t let the guardians get you.'
disabled: true
x: 89 x: 89
y: 82 y: 82
nextLevels:
continue: 'defence-of-plainswood'
} }
{ {
name: 'Defence of Plainswood' name: 'Defence of Plainswood'
@ -718,7 +872,6 @@ hero = [
id: 'defence-of-plainswood' id: 'defence-of-plainswood'
original: '541b67f71ccc8eaae19f3c62' original: '541b67f71ccc8eaae19f3c62'
description: 'Protect the peasants from the pursuing ogres.' description: 'Protect the peasants from the pursuing ogres.'
disabled: true
x: 95.31 x: 95.31
y: 88.26 y: 88.26
} }
@ -781,7 +934,7 @@ hero = [
] ]
campaigns = [ WorldMapView.campaigns = campaigns = [
#{id: 'beginner', name: 'Beginner Campaign', description: '... in which you learn the wizardry of programming.', levels: tutorials, color: "rgb(255, 80, 60)"} #{id: 'beginner', name: 'Beginner Campaign', description: '... in which you learn the wizardry of programming.', levels: tutorials, color: "rgb(255, 80, 60)"}
#{id: 'multiplayer', name: 'Multiplayer Arenas', description: '... in which you code head-to-head against other players.', levels: arenas, color: "rgb(80, 5, 60)"} #{id: 'multiplayer', name: 'Multiplayer Arenas', description: '... in which you code head-to-head against other players.', levels: arenas, color: "rgb(80, 5, 60)"}
#{id: 'dev', name: 'Random Harder Levels', description: '... in which you learn the interface while doing something a little harder.', levels: experienced, color: "rgb(80, 60, 255)"} #{id: 'dev', name: 'Random Harder Levels', description: '... in which you learn the interface while doing something a little harder.', levels: experienced, color: "rgb(80, 60, 255)"}

View file

@ -18,6 +18,10 @@ module.exports = class HeroVictoryModal extends ModalView
subscriptions: subscriptions:
'ladder:game-submitted': 'onGameSubmitted' 'ladder:game-submitted': 'onGameSubmitted'
events:
'click #continue-button': 'onClickContinue'
'click .next-level-branch-button': 'onClickNextLevelBranch'
constructor: (options) -> constructor: (options) ->
super(options) super(options)
@session = options.session @session = options.session
@ -33,6 +37,14 @@ module.exports = class HeroVictoryModal extends ModalView
@waitingToContinueSince = new Date() @waitingToContinueSince = new Date()
Backbone.Mediator.publish 'audio-player:play-sound', trigger: 'victory' Backbone.Mediator.publish 'audio-player:play-sound', trigger: 'victory'
destroy: ->
clearInterval @sequentialAnimationInterval
super()
onHidden: ->
Backbone.Mediator.publish 'music-player:exit-menu', {}
super()
onAchievementsLoaded: -> onAchievementsLoaded: ->
thangTypeOriginals = [] thangTypeOriginals = []
achievementIDs = [] achievementIDs = []
@ -102,13 +114,20 @@ module.exports = class HeroVictoryModal extends ModalView
c.me = me c.me = me
c.readyToRank = @level.get('type', true) is 'hero-ladder' and @session.readyToRank() c.readyToRank = @level.get('type', true) is 'hero-ladder' and @session.readyToRank()
c.level = @level c.level = @level
@continueLevelLink = @getNextLevelLink 'continue'
@morePracticeLevelLink = me.isAdmin() and @getNextLevelLink 'more_practice'
@skipAheadLevelLink = me.isAdmin() and @getNextLevelLink 'skip_ahead'
c.continueButtons = [
{key: 'skip_ahead', link: @skipAheadLevelLink, 'choice-explicit': 'skip', 'choice-implicit': 'too_easy'}
{key: 'continue', link: @continueLevelLink, 'choice-explicit': 'next_level', 'choice-implicit': 'just_right'}
{key: 'more_practice', link: @morePracticeLevelLink, 'choice-explicit': 'more_practice', 'choice-implicit': 'too_hard'}
]
return c return c
afterRender: -> afterRender: ->
super() super()
return unless @supermodel.finished() return unless @supermodel.finished()
@playSelectionSound hero, true for original, hero of @thangTypes # Preload them @playSelectionSound hero, true for original, hero of @thangTypes # Preload them
@$el.addClass 'with-sign-up' if me.get('anonymous')
@updateSavingProgressStatus() @updateSavingProgressStatus()
@$el.find('#victory-header').delay(250).queue(-> @$el.find('#victory-header').delay(250).queue(->
$(@).removeClass('out').dequeue() $(@).removeClass('out').dequeue()
@ -134,6 +153,7 @@ module.exports = class HeroVictoryModal extends ModalView
panel.delay(500) panel.delay(500)
panel.queue(-> complete()) panel.queue(-> complete())
@animationComplete = not @animatedPanels.length @animationComplete = not @animatedPanels.length
complete() if @animationComplete
if @level.get('type', true) is 'hero-ladder' if @level.get('type', true) is 'hero-ladder'
@ladderSubmissionView = new LadderSubmissionView session: @session, level: @level @ladderSubmissionView = new LadderSubmissionView session: @session, level: @level
@insertSubView @ladderSubmissionView, @$el.find('.ladder-submission-view') @insertSubView @ladderSubmissionView, @$el.find('.ladder-submission-view')
@ -216,16 +236,18 @@ module.exports = class HeroVictoryModal extends ModalView
clearInterval @sequentialAnimationInterval clearInterval @sequentialAnimationInterval
@animationComplete = true @animationComplete = true
@updateSavingProgressStatus() @updateSavingProgressStatus()
Backbone.Mediator.publish 'music-player:enter-menu', terrain: @level.get('terrain', true)
updateSavingProgressStatus: -> updateSavingProgressStatus: ->
return unless @animationComplete return unless @animationComplete
@$el.find('#saving-progress-label').toggleClass('hide', @readyToContinue) @$el.find('#saving-progress-label').toggleClass('hide', @readyToContinue)
@$el.find('#continue-button').toggleClass('hide', not @readyToContinue) @$el.find('.next-level-button').toggleClass('hide', not @readyToContinue)
@$el.find('.sign-up-poke').toggleClass('hide', not @readyToContinue) @$el.find('.sign-up-poke').toggleClass('hide', not @readyToContinue)
onGameSubmitted: (e) -> onGameSubmitted: (e) ->
ladderURL = "/play/ladder/#{@level.get('slug')}#my-matches" ladderURL = "/play/ladder/#{@level.get('slug')}#my-matches"
Backbone.Mediator.publish 'router:navigate', route: ladderURL # Preserve the supermodel as we navigate back to the ladder.
Backbone.Mediator.publish 'router:navigate', route: ladderURL, viewClass: require('views/play/ladder/LadderView'), viewArgs: [{supermodel: @supermodel}]
playSelectionSound: (hero, preload=false) -> playSelectionSound: (hero, preload=false) ->
return unless sounds = hero.get('soundTriggers')?.selected return unless sounds = hero.get('soundTriggers')?.selected
@ -236,8 +258,36 @@ module.exports = class HeroVictoryModal extends ModalView
else else
AudioPlayer.playSound name, 1 AudioPlayer.playSound name, 1
# TODO: award heroes/items and play an awesome sound when you get one # Branching group testing
destroy: -> getNextLevel: (type) ->
clearInterval @sequentialAnimationInterval for campaign in require('views/play/WorldMapView').campaigns
super() break if levelInfo
for level in campaign.levels
if level.id is @level.get 'slug'
levelInfo = level
break
levelInfo?.nextLevels?[type] # 'more_practice', 'skip_ahead', 'continue'
getNextLevelLink: (type) ->
return '/play' unless nextLevel = @getNextLevel type
"play?next=#{nextLevel}"
onClickContinue: (e) ->
nextLevelLink = @continueLevelLink
if me.getBranchingGroup() is 'all-practice' and @morePracticeLevelLink
nextLevelLink = @morePracticeLevelLink
skipPrompt = me.getBranchingGroup() in ['no-practice', 'all-practice']
skipPrompt ||= not (@skipAheadLevelLink or @morePractiveLevelLink) and me.getBranchingGroup() is 'choice-explicit'
if skipPrompt
# Preserve the supermodel as we navigate back to the world map.
Backbone.Mediator.publish 'router:navigate', route: nextLevelLink, viewClass: require('views/play/WorldMapView'), viewArgs: [{supermodel: @supermodel}]
else
# Hide everything except the buttons prompting them for which kind of next level to do
@$el.find('.modal-footer, .modal-body > *').hide()
@$el.find('.next-levels-prompt').show()
onClickNextLevelBranch: (e) ->
application.tracker?.trackEvent 'Branch Selected', level: @level.get('slug'), label: @level.get('slug'), branch: $(e.target).data('branch-key'), branchingGroup: me.getBranchingGroup()
# Preserve the supermodel as we navigate back to world map.
Backbone.Mediator.publish 'router:navigate', route: '/play', viewClass: require('views/play/WorldMapView'), viewArgs: [{supermodel: @supermodel}]

View file

@ -47,7 +47,7 @@ module.exports = class SpellPaletteEntryView extends CocoView
).on 'show.bs.popover', => ).on 'show.bs.popover', =>
Backbone.Mediator.publish 'tome:palette-hovered', thang: @thang, prop: @doc.name, entry: @ Backbone.Mediator.publish 'tome:palette-hovered', thang: @thang, prop: @doc.name, entry: @
soundIndex = Math.floor(Math.random() * 4) soundIndex = Math.floor(Math.random() * 4)
Backbone.Mediator.publish 'audio-player:play-sound', trigger: "spell-palette-entry-open-{soundIndex}", volume: 0.75 Backbone.Mediator.publish 'audio-player:play-sound', trigger: "spell-palette-entry-open-#{soundIndex}", volume: 0.75
onMouseEnter: (e) -> onMouseEnter: (e) ->
# Make sure the doc has the updated Thang so it can regenerate its prop value # Make sure the doc has the updated Thang so it can regenerate its prop value

View file

@ -75,7 +75,7 @@ module.exports = class SpellPaletteView extends CocoView
columns = ({items: [], nEntries: 0} for i in [0 ... nColumns]) columns = ({items: [], nEntries: 0} for i in [0 ... nColumns])
nRows = 0 nRows = 0
for group, entries of @entryGroups for group, entries of @entryGroups
shortestColumn = _.sortBy(columns, (column) -> column.nEntries)[0] continue unless shortestColumn = _.sortBy(columns, (column) -> column.nEntries)[0]
shortestColumn.nEntries += Math.max 2, entries.length shortestColumn.nEntries += Math.max 2, entries.length
shortestColumn.items.push @entryGroupElements[group] shortestColumn.items.push @entryGroupElements[group]
nRows = Math.max nRows, shortestColumn.nEntries nRows = Math.max nRows, shortestColumn.nEntries

View file

@ -197,13 +197,15 @@ module.exports = class SpellView extends CocoView
@zatanna = new Zatanna @ace, @zatanna = new Zatanna @ace,
basic: false basic: false
liveCompletion: false liveCompletion: false
snippets: @autocomplete
snippetsLangDefaults: false snippetsLangDefaults: false
completers: completers:
keywords: false keywords: false
text: false snippets: @autocomplete
text: @autocomplete
autoLineEndings: autoLineEndings:
javascript: ';' javascript: ';'
popupFontSizePx: 16
popupWidthPx: 380
updateAutocomplete: (@autocomplete) -> updateAutocomplete: (@autocomplete) ->
@zatanna?.set 'snippets', @autocomplete @zatanna?.set 'snippets', @autocomplete
@ -639,7 +641,7 @@ module.exports = class SpellView extends CocoView
# TODO: move this whole thing into SpellDebugView or somewhere? # TODO: move this whole thing into SpellDebugView or somewhere?
@highlightComments() unless @destroyed @highlightComments() unless @destroyed
flow ?= @spellThang?.castAether?.flow flow ?= @spellThang?.castAether?.flow
return unless flow return unless flow and @thang
executed = [] executed = []
executedRows = {} executedRows = {}
matched = false matched = false

View file

@ -32,7 +32,7 @@ setupExpressMiddleware = (app) ->
express.logger.format('prod', productionLogging) express.logger.format('prod', productionLogging)
app.use(express.logger('prod')) app.use(express.logger('prod'))
app.use express.compress filter: (req, res) -> app.use express.compress filter: (req, res) ->
return false if req.headers.host is 'codecombat.com' # Cloudflare will gzip it for us on codecombat.com #return false if req.headers.host is 'codecombat.com' # CloudFlare will gzip it for us on codecombat.com # But now it's disabled.
compressible res.getHeader('Content-Type') compressible res.getHeader('Content-Type')
else else
app.use(express.logger('dev')) app.use(express.logger('dev'))