diff --git a/app/styles/mixins.sass b/app/styles/mixins.sass index 18f695aac..c3ca386c1 100644 --- a/app/styles/mixins.sass +++ b/app/styles/mixins.sass @@ -86,6 +86,12 @@ -ms-flex-pack: justify justify-content: space-between +@mixin flex-justify-center() + -webkit-box-pack: center + -webkit-justify-content: center + -ms-flex-pack: center + justify-content: center + @mixin flex-align-content-start() -webkit-align-content: flex-start -ms-flex-align-content: flex-start diff --git a/app/styles/play/level/modal/hero-victory-modal.sass b/app/styles/play/level/modal/hero-victory-modal.sass index 62b31642e..f044a91a3 100644 --- a/app/styles/play/level/modal/hero-victory-modal.sass +++ b/app/styles/play/level/modal/hero-victory-modal.sass @@ -1,18 +1,31 @@ +@import "app/styles/mixins" +@import "app/styles/bootstrap/variables" + #hero-victory-modal + //- Top-level modal container + .modal-dialog + margin-top: 15px + padding-top: 0 //- Header .background-wrapper - background: url("/images/pages/play/level/modal/victory_modal_background.png") - height: 650px + //background: url("/images/pages/play/level/modal/victory_modal_background.png") width: 550px - + border-width: 25px + border-image: url("/images/pages/play/level/modal/victory_modal_background.png") 25 fill round + border-radius: 10px + #victory-header display: block - margin: 40px auto 0 + margin: 15px auto 0 + @include transition(0.25s ease-in) + + &.out + margin-top: -100px .modal-header - height: 110px + height: 85px border: none @@ -28,15 +41,12 @@ margin: 5px auto position: relative - -webkit-transition-duration: 1s - -moz-transition-duration: 1s - -o-transition-duration: 1s - transition-duration: 1s + @include transition-duration(1s) - -webkit-filter: grayscale(100%) - -moz-filter: grayscale(100%) - -o-filter: grayscale(100%) - filter: grayscale(100%) + -webkit-filter: grayscale(100%) brightness(75%) + -moz-filter: grayscale(100%) brightness(75%) + -o-filter: grayscale(100%) brightness(75%) + filter: grayscale(100%) brightness(75%) &.earned -webkit-filter: none @@ -44,7 +54,11 @@ -o-filter: none filter: none + .achievement-description + @include opacity(1) + .achievement-description + @include opacity(0.75) position: absolute text-align: center left: 95px @@ -54,25 +68,36 @@ white-space: nowrap overflow: hidden text-overflow: ellipsis - + .achievement-rewards position: absolute left: 25px right: 23px top: 41px bottom: 18px - + @include flexbox() + @include flex-justify-center() //- Reward panels .reward-panel - background: url("/images/pages/play/level/modal/reward_plate.png") background: url("/images/pages/play/level/modal/reward_plate.png") width: 77px height: 85px float: left margin: 0 1.8px position: relative + z-index: 1 + @include transition(0.25s ease) + + &.animating + @include scale(1.5) + z-index: 2 + + .reward-text + font-size: 18px + overflow: visible + bottom: 9px .reward-image-container top: 8px @@ -81,21 +106,11 @@ width: 56px position: relative - -webkit-transform: scale(0, 0) - -moz-transform: scale(0, 0) - -o-transform: scale(0, 0) - transform: scale(0, 0) - - -webkit-transition-duration: 0.5s - -moz-transition-duration: 0.5s - -o-transition-duration: 0.5s - transition-duration: 0.5s + @include scale(0) + @include transition-duration(0.5s) &.show - -webkit-transform: scale(1, 1) - -moz-transform: scale(1, 1) - -o-transform: scale(1, 1) - transform: scale(1, 1) + @include scale(1) img margin: 0 @@ -103,17 +118,8 @@ top: 50% left: 50% margin-right: -50% - - -webkit-transition-duration: 0.5s - -moz-transition-duration: 0.5s - -o-transition-duration: 0.5s - transition-duration: 0.5s - - -webkit-transform: translate(-50%, -50%) - -moz-transform: translate(-50%, -50%) - -o-transform: translate(-50%, -50%) - transform: translate(-50%, -50%) - + @include transition-duration(0.5s) + @include translate(-50%, -50%) max-width: 56px max-height: 55px @@ -130,59 +136,65 @@ white-space: nowrap overflow: hidden text-overflow: ellipsis - //- Pulse effect - @-webkit-keyframes pulse + +keyframes(rewardPulse) from - -webkit-transform: translate(-50%, -50%) scale(1.0) + max-width: 56px + max-height: 55px 50% - -webkit-transform: translate(-50%, -50%) scale(1.3) + width: 66px + max-width: 66px + max-height: 66px to - -webkit-transform: translate(-50%, -50%) scale(1.0) + max-width: 56px + max-height: 55px - @-moz-keyframes pulse - from - -moz-transform: translate(-50%, -50%) scale(1.0) - 50% - -moz-transform: translate(-50%, -50%) scale(1.3) - to - -moz-transform: translate(-50%, -50%) scale(1.0) + .xp .pulse + @include animation(rewardPulse 0.15s infinite) - @-o-keyframes pulse - from - -o-transform: translate(-50%, -50%) scale(1.0) - 50% - -o-transform: translate(-50%, -50%) scale(1.3) - to - -o-transform: translate(-50%, -50%) scale(1.0) - - @keyframes pulse - from - transform: translate(-50%, -50%) scale(1.0) - 50% - transform: translate(-50%, -50%) scale(1.3) - to - transform: translate(-50%, -50%) scale(1.0) - - .pulse - -webkit-animation: pulse 0.5s infinite - -moz-animation: pulse 0.5s infinite - -o-animation: pulse 0.5s infinite - animation: pulse 0.5s infinite - + .gems .pulse + @include animation(rewardPulse 0.25s infinite) //- Footer .modal-content - height: 650px // so the footer appears at the bottom - + 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 position: absolute - bottom: 20px + bottom: -20px left: 20px right: 20px #totals - color: white \ No newline at end of file + color: white + + p.sign-up-poke + position: absolute + bottom: 60px + right: 20px + color: white + + .sign-up-button + float: right + margin-left: 10px + +html.no-borderimage + #hero-victory-modal + .background-wrapper + background: url("/images/pages/play/level/modal/victory_modal_background.png") + height: 650px + #victory-header + margin-top: 40px + .modal-header + height: 110px + .modal-content + height: 650px + padding-bottom: 0 + .modal-footer + bottom: 20px diff --git a/app/templates/play/level/modal/hero-victory-modal.jade b/app/templates/play/level/modal/hero-victory-modal.jade index dd0d78d22..32d4f2fbb 100644 --- a/app/templates/play/level/modal/hero-victory-modal.jade +++ b/app/templates/play/level/modal/hero-victory-modal.jade @@ -1,6 +1,6 @@ extends /templates/modal/modal_base block modal-header-content - img(src="/images/pages/play/level/modal/victory_word.png")#victory-header + img(src="/images/pages/play/level/modal/victory_word.png")#victory-header.out block modal-body-content @@ -14,21 +14,21 @@ block modal-body-content div.achievement-rewards - var worth = achievement.get('worth', true); if worth - .reward-panel.numerical(data-number=worth, data-number-unit='xp') + .reward-panel.numerical.xp(data-number=worth, data-number-unit='xp') .reward-image-container(class=animate?'':'show') img(src="/images/pages/play/level/modal/reward_icon_xp.png") - .reward-text= animate ? 'x0' : '+'+worth + .reward-text= animate ? '+0' : '+'+worth if rewards.gems - .reward-panel.numerical(data-number=rewards.gems, data-number-unit='gem') + .reward-panel.numerical.gems(data-number=rewards.gems, data-number-unit='gem') .reward-image-container(class=animate?'':'show') img(src="/images/pages/play/level/modal/reward_icon_gems.png") - .reward-text= animate ? 'x0' : '+'+rewards.gems + .reward-text= animate ? '+0' : '+'+rewards.gems if rewards.heroes for hero in rewards.heroes - var hero = thangTypes[hero]; - .reward-panel + .reward-panel.hero .reward-image-container(class=animate?'':'show') img(src=hero.getPortraitURL()) .reward-text= hero.get('name') @@ -36,7 +36,7 @@ block modal-body-content if rewards.items for item in rewards.items - var item = thangTypes[item]; - .reward-panel + .reward-panel.item .reward-image-container(class=animate?'':'show') img(src=item.getPortraitURL()) .reward-text= item.get('name') @@ -52,3 +52,8 @@ block modal-footer-content button.btn.btn-warning.hide#saving-progress-label(disabled, data-i18n="play_level.victory_saving_progress") Saving Progress a.btn.btn-success.world-map-button.hide#continue-button(href="/play-hero", data-dismiss="modal", data-i18n="play_level.victory_play_continue") Continue + + if me.get('anonymous') + p.sign-up-poke + 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! diff --git a/app/views/play/level/modal/HeroVictoryModal.coffee b/app/views/play/level/modal/HeroVictoryModal.coffee index 5f4fe7d83..b21805334 100644 --- a/app/views/play/level/modal/HeroVictoryModal.coffee +++ b/app/views/play/level/modal/HeroVictoryModal.coffee @@ -25,6 +25,7 @@ module.exports = class HeroVictoryModal extends ModalView @achievements = @supermodel.loadCollection(achievements, 'achievements').model @listenToOnce @achievements, 'sync', @onAchievementsLoaded @readyToContinue = false + Backbone.Mediator.publish 'audio-player:play-sound', trigger: 'victory' onAchievementsLoaded: -> thangTypeOriginals = [] @@ -74,19 +75,27 @@ module.exports = class HeroVictoryModal extends ModalView c.achievements = @achievements.models # for testing the three states -# if c.achievements.length -# c.achievements = [c.achievements[0].clone(), c.achievements[0].clone(), c.achievements[0].clone()] -# for achievement, index in c.achievements -# achievement.completed = index > 0 -# achievement.completedAWhileAgo = index > 1 + #if c.achievements.length + # c.achievements = [c.achievements[0].clone(), c.achievements[0].clone(), c.achievements[0].clone()] + #for achievement, index in c.achievements + ## achievement.completed = index > 0 + ## achievement.completedAWhileAgo = index > 1 + # achievement.completed = true + # achievement.completedAWhileAgo = false + # achievement.attributes.worth = (index + 1) * achievement.get('worth') + # rewards = achievement.get('rewards') + # rewards.gems *= (index + 1) c.thangTypes = @thangTypes + c.me = me return c afterRender: -> super() return unless @supermodel.finished() + @$el.addClass 'with-sign-up' if me.get('anonymous') @updateSavingProgressStatus() + @$el.find('#victory-header').delay(250).queue(-> $(@).removeClass('out').dequeue()) complete = _.once(_.bind(@beginAnimateNumbers, @)) @animatedPanels = $() panels = @$el.find('.achievement-panel') @@ -94,15 +103,15 @@ module.exports = class HeroVictoryModal extends ModalView panel = $(panel) continue unless panel.data('animate') @animatedPanels = @animatedPanels.add(panel) - panel.delay(500) + panel.delay(500) # Waiting for victory header to show up and fall panel.queue(-> - $(this).addClass('earned') # animate out the grayscale - $(this).dequeue() + $(@).addClass('earned') # animate out the grayscale + $(@).dequeue() ) panel.delay(500) panel.queue(-> - $(this).find('.reward-image-container').addClass('show') - $(this).dequeue() + $(@).find('.reward-image-container').addClass('show') + $(@).dequeue() ) panel.delay(500) panel.queue(-> complete()) @@ -116,27 +125,59 @@ module.exports = class HeroVictoryModal extends ModalView unit: $(panel).data('number-unit') }) - # TODO: mess with this more later. Doesn't seem to work, often times will pulse background red rather than animate -# itemPanel.rootEl.find('.reward-image-container img').addClass('pulse') for itemPanel in @numericalItemPanels - @numberAnimationStart = new Date() @totalXP = 0 @totalXP += panel.number for panel in @numericalItemPanels when panel.unit is 'xp' @totalGems = 0 @totalGems += panel.number for panel in @numericalItemPanels when panel.unit is 'gem' @gemEl = $('#gem-total') @XPEl = $('#xp-total') - @numberAnimationInterval = setInterval(@tickNumberAnimation, 15 / 1000) + @totalXPAnimated = @totalGemsAnimated = @lastTotalXP = @lastTotalGems = 0 + @numberAnimationStart = new Date() + @numberAnimationInterval = setInterval(@tickNumberAnimation, 1000 / 60) tickNumberAnimation: => - pct = Math.min(1, (new Date() - @numberAnimationStart) / 1500) - panel.textEl.text('+'+parseInt(panel.number*pct)) for panel in @numericalItemPanels - @XPEl.text('+'+parseInt(@totalXP * pct)) - @gemEl.text('+'+parseInt(@totalGems * pct)) - @endAnimateNumbers() if pct is 1 + # TODO: make sure the animation pulses happen when the numbers go up and sounds play (up to a max speed) + return @endAnimateNumbers() unless panel = @numericalItemPanels[0] + duration = Math.log10(panel.number + 1) * 1000 + ratio = @getEaseRatio (new Date() - @numberAnimationStart), duration + if panel.unit is 'xp' + totalXP = @totalXPAnimated + Math.floor(ratio * panel.number) + if totalXP isnt @lastTotalXP + panel.textEl.text('+' + totalXP) + @XPEl.text('+' + totalXP) + xpTrigger = 'xp-' + (totalXP % 6) # 6 xp sounds + Backbone.Mediator.publish 'audio-player:play-sound', trigger: xpTrigger, volume: 0.5 + ratio / 2 + @lastTotalXP = totalXP + else + totalGems = @totalGemsAnimated + Math.floor(ratio * panel.number) + if totalGems isnt @lastTotalGems + panel.textEl.text('+' + totalGems) + @gemEl.text('+' + totalGems) + gemTrigger = 'gem-' + (parseInt(panel.number * ratio) % 4) # 4 gem sounds + Backbone.Mediator.publish 'audio-player:play-sound', trigger: gemTrigger, volume: 0.5 + ratio / 2 + @lastTotalGems = totalGems + if ratio is 1 + panel.rootEl.removeClass('animating').find('.reward-image-container img').removeClass('pulse') + @numberAnimationStart = new Date() + if panel.unit is 'xp' + @totalXPAnimated += panel.number + else + @totalGemsAnimated += panel.number + @numericalItemPanels.shift() + return + panel.rootEl.addClass('animating').find('.reward-image-container img').addClass('pulse') + + getEaseRatio: (timeSinceStart, duration) -> + # Ease in/out quadratic - http://gizma.com/easing/ + timeSinceStart = Math.min timeSinceStart, duration + t = 2 * timeSinceStart / duration + if t < 1 + return 0.5 * t * t + --t + -0.5 * (t * (t - 2) - 1) endAnimateNumbers: -> - @$el.find('.pulse').removeClass('pulse') - clearInterval(@numberAnimationInterval) + clearInterval @numberAnimationInterval @animationComplete = true @updateSavingProgressStatus() @@ -144,3 +185,9 @@ module.exports = class HeroVictoryModal extends ModalView return unless @animationComplete @$el.find('#saving-progress-label').toggleClass('hide', @readyToContinue) @$el.find('#continue-button').toggleClass('hide', not @readyToContinue) + + # TODO: award heroes/items and play an awesome sound when you get one + + destroy: -> + clearInterval @numberAnimationInterval + super() diff --git a/app/views/play/level/tome/Problem.coffee b/app/views/play/level/tome/Problem.coffee index 2ac28e0fe..5c8e3d2e1 100644 --- a/app/views/play/level/tome/Problem.coffee +++ b/app/views/play/level/tome/Problem.coffee @@ -1,6 +1,5 @@ ProblemAlertView = require './ProblemAlertView' Range = ace.require('ace/range').Range -UserCodeProblem = require 'models/UserCodeProblem' module.exports = class Problem annotation: null @@ -11,7 +10,6 @@ module.exports = class Problem @buildAlertView() if withAlert @buildMarkerRange() if isCast Backbone.Mediator.publish("problem:problem-created", line:@annotation.row, text: @annotation.text) if application.isIPadApp - @saveUserCodeProblem() if isCast destroy: -> unless @alertView?.destroyed @@ -50,21 +48,3 @@ module.exports = class Problem @ace.getSession().removeMarker @markerRange.id @markerRange.start.detach() @markerRange.end.detach() - - saveUserCodeProblem: () -> - @userCodeProblem = new UserCodeProblem() - @userCodeProblem.set 'code', @aether.raw - if @aetherProblem.range - rawLines = @aether.raw.split '\n' - errorLines = rawLines.slice @aetherProblem.range[0].row, @aetherProblem.range[1].row + 1 - @userCodeProblem.set 'codeSnippet', errorLines.join '\n' - @userCodeProblem.set 'errHint', @aetherProblem.hint if @aetherProblem.hint - @userCodeProblem.set 'errId', @aetherProblem.id if @aetherProblem.id - @userCodeProblem.set 'errLevel', @aetherProblem.level if @aetherProblem.level - @userCodeProblem.set 'errMessage', @aetherProblem.message if @aetherProblem.message - @userCodeProblem.set 'errRange', @aetherProblem.range if @aetherProblem.range - @userCodeProblem.set 'errType', @aetherProblem.type if @aetherProblem.type - @userCodeProblem.set 'language', @aether.language.id if @aether.language?.id - @userCodeProblem.set 'levelID', @levelID if @levelID - @userCodeProblem.save() - null \ No newline at end of file diff --git a/app/views/play/level/tome/SpellView.coffee b/app/views/play/level/tome/SpellView.coffee index 48334248b..e77f689ab 100644 --- a/app/views/play/level/tome/SpellView.coffee +++ b/app/views/play/level/tome/SpellView.coffee @@ -8,6 +8,7 @@ Problem = require './Problem' SpellDebugView = require './SpellDebugView' SpellToolbarView = require './SpellToolbarView' LevelComponent = require 'models/LevelComponent' +UserCodeProblem = require 'models/UserCodeProblem' module.exports = class SpellView extends CocoView id: 'spell-view' @@ -63,6 +64,7 @@ module.exports = class SpellView extends CocoView @listenTo(@session, 'change:multiplayer', @onMultiplayerChanged) @spell = options.spell @problems = [] + @savedProblems = {} # Cache saved user code problems to prevent duplicates @writable = false unless me.team in @spell.permissions.readwrite # TODO: make this do anything @highlightCurrentLine = _.throttle @highlightCurrentLine, 100 $(window).on 'resize', @onWindowResize @@ -486,6 +488,7 @@ module.exports = class SpellView extends CocoView continue if key = aetherProblem.userInfo?.key and key of seenProblemKeys seenProblemKeys[key] = true if key @problems.push problem = new Problem aether, aetherProblem, @ace, isCast and problemIndex is 0, isCast, @spell.levelID + @saveUserCodeProblem(aether, aetherProblem) if isCast annotations.push problem.annotation if problem.annotation @aceSession.setAnnotations annotations @highlightCurrentLine aether.flow unless _.isEmpty aether.flow @@ -498,6 +501,29 @@ module.exports = class SpellView extends CocoView Backbone.Mediator.publish 'tome:problems-updated', spell: @spell, problems: @problems, isCast: isCast @ace.resize() + saveUserCodeProblem: (aether, aetherProblem) -> + # Skip duplicate problems + hashValue = aether.raw + aetherProblem.message + return if hashValue of @savedProblems + @savedProblems[hashValue] = true + # Save new problem + @userCodeProblem = new UserCodeProblem() + @userCodeProblem.set 'code', aether.raw + if aetherProblem.range + rawLines = aether.raw.split '\n' + errorLines = rawLines.slice aetherProblem.range[0].row, aetherProblem.range[1].row + 1 + @userCodeProblem.set 'codeSnippet', errorLines.join '\n' + @userCodeProblem.set 'errHint', aetherProblem.hint if aetherProblem.hint + @userCodeProblem.set 'errId', aetherProblem.id if aetherProblem.id + @userCodeProblem.set 'errLevel', aetherProblem.level if aetherProblem.level + @userCodeProblem.set 'errMessage', aetherProblem.message if aetherProblem.message + @userCodeProblem.set 'errRange', aetherProblem.range if aetherProblem.range + @userCodeProblem.set 'errType', aetherProblem.type if aetherProblem.type + @userCodeProblem.set 'language', aether.language.id if aether.language?.id + @userCodeProblem.set 'levelID', @spell.levelID if @spell.levelID + @userCodeProblem.save() + null + # Autocast: # Goes immediately if the code is a) changed and b) complete/valid and c) the cursor is at beginning or end of a line # We originally thought it would: