diff --git a/app/lib/surface/Lank.coffee b/app/lib/surface/Lank.coffee index 5adbbb4d5..f951c9828 100644 --- a/app/lib/surface/Lank.coffee +++ b/app/lib/surface/Lank.coffee @@ -638,8 +638,9 @@ module.exports = Lank = class Lank extends CocoClass onDialogue: (e) -> return unless @thang?.id is e.spriteID - label = @addLabel 'dialogue', Label.STYLE_DIALOGUE - label.setText e.blurb or '...' + unless @thang?.id is 'Hero Placeholder' # Don't show these for heroes, because they aren't actually first-person, just LevelDialogueView narration + label = @addLabel 'dialogue', Label.STYLE_DIALOGUE + label.setText e.blurb or '...' sound = e.sound ? AudioPlayer.soundForDialogue e.message, @thangType.get 'soundTriggers' @dialogueSoundInstance?.stop() if @dialogueSoundInstance = @playSound sound, false diff --git a/app/locale/en.coffee b/app/locale/en.coffee index fca690816..184f94208 100644 --- a/app/locale/en.coffee +++ b/app/locale/en.coffee @@ -235,7 +235,6 @@ tome_available_spells: "Available Spells" tome_your_skills: "Your Skills" tome_current_method: "Current Method" - hud_continue: "Continue (shift+space)" hud_continue_short: "Continue" code_saved: "Code Saved" skip_tutorial: "Skip (esc)" diff --git a/app/styles/play/level.sass b/app/styles/play/level.sass index 35462f286..ecfeb842d 100644 --- a/app/styles/play/level.sass +++ b/app/styles/play/level.sass @@ -50,6 +50,8 @@ $level-resize-transition-time: 0.5s #stop-real-time-playback-button display: block z-index: 20 + #level-dialogue-view + display: none .level-content margin: 0px auto diff --git a/app/styles/play/level/hud.sass b/app/styles/play/level/hud.sass index cd978517a..be3502933 100644 --- a/app/styles/play/level/hud.sass +++ b/app/styles/play/level/hud.sass @@ -8,9 +8,11 @@ overflow: visible &.controls-disabled - @include opacity(0.5) pointer-events: none + .wood-background, .hinge, .avatar-wrapper-container, .center + @include filter(brightness(50%)) + .wood-background position: absolute left: 0 @@ -19,7 +21,7 @@ background-size: auto 100% width: 100% height: 100px - z-index: 2 + z-index: 4 .hinge position: absolute @@ -28,7 +30,7 @@ width: 27px height: 44px background-size: contain - z-index: 2 + z-index: 4 pointer-events: none .hinge-0 @@ -50,7 +52,7 @@ left: 18% left: -webkit-calc(50% - (560px - 100px) / 2 - 10px) left: calc(50% - (560px - 100px) / 2 - 10px) - z-index: 3 + z-index: 5 .thang-canvas-wrapper width: 80px @@ -99,7 +101,7 @@ font-family: Open Sans Condensed font-weight: bold font-size: 16px - z-index: 2 + z-index: 4 @include transition(0.5s ease) &:hover diff --git a/app/styles/play/level/level-dialogue-view.sass b/app/styles/play/level/level-dialogue-view.sass new file mode 100644 index 000000000..218397764 --- /dev/null +++ b/app/styles/play/level/level-dialogue-view.sass @@ -0,0 +1,91 @@ +@import "app/styles/mixins" +@import "app/styles/bootstrap/variables" + +#level-dialogue-view + +keyframes(speakingPulse) + from + @include box-shadow(0px 0px 8px #333) + color: white + 50% + @include box-shadow(0px 0px 35px skyblue) + color: skyblue + to + @include box-shadow(0px 0px 8px #333) + color: white + + width: 417px + height: 296px + background: transparent url(/images/level/code_palette_wood_background.png) + background-size: 100% auto + position: absolute + bottom: -296px + 40px + //left: -webkit-calc(27.5% - 417px / 2) + left: -webkit-calc(55% - 417px) + // Bounce in + @include transition(1s cubic-bezier(.17,.89,.42,1.36)) + z-index: 2 + + &.active + display: block + bottom: -20px + + &.speaking + .dialogue-area + .bubble + @include animation(speakingPulse 1.5s infinite) + + .dialogue-area + position: relative + height: 100% + width: 100% + z-index: 1 + + .bubble + position: relative + margin: 20px + padding: 10px 10px 30px 10px + color: white + font-weight: bold + background: rgb(45, 35, 234) + border: black solid 1px + border-radius: 10px + font-size: 18px + line-height: 20px + + strong + color: #09B057 + + .hud-hint + font-weight: normal + color: #ddd + font-size: 14px + line-height: 16px + vertical-align: middle + + .enter + position: absolute + right: 10px + bottom: 10px + div.dot + background: #337 + width: 8px + height: 8px + position: absolute + right: 8px + top: 9px + border-radius: 5px + + button, .alert + padding: 2px 5px + + .enter button.with-dot + padding-right: 20px + + h3 + margin: 0 + font-size: 16px + line-height: 16px + color: #338 + + button + margin-left: 10px diff --git a/app/styles/play/level/playback.sass b/app/styles/play/level/playback.sass index 93176ead3..87b9ebadd 100644 --- a/app/styles/play/level/playback.sass +++ b/app/styles/play/level/playback.sass @@ -12,7 +12,11 @@ background-size: 100% 100% // Counteract 50px height of absolutely positioned control bar, but overlap by 10px of jagged transparent top. margin-top: 50px - 10px - z-index: 2 + z-index: 3 + + &.controls-disabled + pointer-events: none + @include filter(brightness(50%)) button font-size: 26px diff --git a/app/styles/play/level/tome/spell_list_entry.sass b/app/styles/play/level/tome/spell_list_entry.sass index 7d9fe6ab1..60f700c31 100644 --- a/app/styles/play/level/tome/spell_list_entry.sass +++ b/app/styles/play/level/tome/spell_list_entry.sass @@ -43,7 +43,7 @@ &.read-only background: linear-gradient(to bottom, rgba(0,0,0,0.25) 0%,rgba(0,0,0,0.25) 100%), url(/images/level/code_editor_top_bar_wood_background.png) background-size: 100% 100% - > * + > *:not(.spell-tool-buttons) @include opacity(0.5) .thang-avatar-view diff --git a/app/templates/play/level.jade b/app/templates/play/level.jade index b012ac1a8..78a4d3676 100644 --- a/app/templates/play/level.jade +++ b/app/templates/play/level.jade @@ -30,6 +30,8 @@ #thang-hud + #level-dialogue-view + button.btn.btn-lg.btn-warning.banner.header-font#stop-real-time-playback-button(title="Stop real-time playback", data-i18n="play_level.skip") Skip .footer diff --git a/app/templates/play/level/level-dialogue-view.jade b/app/templates/play/level/level-dialogue-view.jade new file mode 100644 index 000000000..75e9ecba6 --- /dev/null +++ b/app/templates/play/level/level-dialogue-view.jade @@ -0,0 +1,2 @@ +.dialogue-area + p.bubble.dialogue-bubble diff --git a/app/views/game-menu/InventoryModal.coffee b/app/views/game-menu/InventoryModal.coffee index 4b0223082..e04588706 100644 --- a/app/views/game-menu/InventoryModal.coffee +++ b/app/views/game-menu/InventoryModal.coffee @@ -495,9 +495,9 @@ requiredGearByLevel = 'descending-further': {feet: 'simple-boots', 'programming-book': 'programmaticon-i'} 'the-second-kithmaze': {feet: 'simple-boots', 'programming-book': 'programmaticon-i'} 'dread-door': {'right-hand': 'simple-sword', 'programming-book': 'programmaticon-i'} - 'known-enemy': {'right-hand': 'simple-sword', 'programming-book': 'programmaticon-i'} - 'master-of-names': {feet: 'simple-boots', 'right-hand': 'simple-sword', 'programming-book': 'programmaticon-i', eyes: 'crude-glasses'} - 'lowly-kithmen': {feet: 'simple-boots', 'right-hand': 'simple-sword', 'programming-book': 'programmaticon-i', eyes: 'crude-glasses'} + 'known-enemy': {'right-hand': 'simple-sword', 'programming-book': 'programmaticon-i', torso: 'leather-tunic'} + 'master-of-names': {feet: 'simple-boots', 'right-hand': 'simple-sword', 'programming-book': 'programmaticon-i', eyes: 'crude-glasses', torso: 'leather-tunic'} + 'lowly-kithmen': {feet: 'simple-boots', 'right-hand': 'simple-sword', 'programming-book': 'programmaticon-i', eyes: 'crude-glasses', torso: 'leather-tunic'} 'closing-the-distance': {feet: 'simple-boots', 'right-hand': 'simple-sword', torso: 'leather-tunic', eyes: 'crude-glasses'} 'tactical-strike': {feet: 'simple-boots', 'right-hand': 'simple-sword', torso: 'leather-tunic', eyes: 'crude-glasses'} 'the-final-kithmaze': {feet: 'simple-boots', 'right-hand': 'simple-sword', torso: 'leather-tunic', 'programming-book': 'programmaticon-i', eyes: 'crude-glasses'} diff --git a/app/views/play/level/DialogueAnimator.coffee b/app/views/play/level/DialogueAnimator.coffee index 3b1c3a2d7..3a7673bd0 100644 --- a/app/views/play/level/DialogueAnimator.coffee +++ b/app/views/play/level/DialogueAnimator.coffee @@ -9,7 +9,7 @@ module.exports = class DialogueAnimator @childrenToAdd = _.map(d[0].childNodes, (e) -> return e) @t0 = new Date() @charsAdded = 0 - @charsPerSecond = 50 + @charsPerSecond = 25 tick: -> if not @charsToAdd and not @childAnimator diff --git a/app/views/play/level/LevelDialogueView.coffee b/app/views/play/level/LevelDialogueView.coffee new file mode 100644 index 000000000..3629ba687 --- /dev/null +++ b/app/views/play/level/LevelDialogueView.coffee @@ -0,0 +1,104 @@ +CocoView = require 'views/kinds/CocoView' +template = require 'templates/play/level/level-dialogue-view' +DialogueAnimator = require './DialogueAnimator' + +module.exports = class LevelDialogueView extends CocoView + id: 'level-dialogue-view' + template: template + + subscriptions: + 'sprite:speech-updated': 'onSpriteDialogue' + 'level:sprite-clear-dialogue': 'onSpriteClearDialogue' + 'level:shift-space-pressed': 'onShiftSpacePressed' + 'level:escape-pressed': 'onEscapePressed' + 'sprite:dialogue-sound-completed': 'onDialogueSoundCompleted' + + events: + 'click': 'onClick' + + onClick: (e) -> + Backbone.Mediator.publish 'tome:focus-editor', {} + + onFrameChanged: (e) -> + @timeProgress = e.progress + @update() + + onSpriteDialogue: (e) -> + return unless e.message + @$el.addClass 'active speaking' + @setMessage e.message, e.mood, e.responses + + window.tracker?.trackEvent 'Heard Sprite', {message: e.message, label: e.message}, ['Google Analytics'] + + onDialogueSoundCompleted: -> + @$el.removeClass 'speaking' + + onSpriteClearDialogue: -> + @$el.removeClass 'active speaking' + + setMessage: (message, mood, responses) -> + message = marked message + # Fix old HTML icons like in the Markdown + message = message.replace /<i class='(.+?)'><\/i>/, "" + clearInterval(@messageInterval) if @messageInterval + @bubble = $('.dialogue-bubble', @$el) + @bubble.removeClass(@lastMood) if @lastMood + @lastMood = mood + @bubble.text('') + group = $('
') + @bubble.append(group) + if responses + @lastResponses = responses + for response in responses + button = $('').text(response.text) + button.addClass response.buttonClass if response.buttonClass + group.append(button) + response.button = $('button:last', group) + else + s = $.i18n.t('play_level.hud_continue_short', defaultValue: 'Continue') + sk = $.i18n.t('play_level.skip_tutorial', defaultValue: 'skip: esc') + if not @escapePressed + group.append('' + sk + '') + group.append($('')) + @lastResponses = null + @animator = new DialogueAnimator(message, @bubble) + @messageInterval = setInterval(@addMoreMessage, 1000 / 30) # 30 FPS + + addMoreMessage: => + if @animator.done() + clearInterval(@messageInterval) + @messageInterval = null + $('.enter', @bubble).removeClass('secret').css('opacity', 0.0).delay(500).animate({opacity: 1.0}, 500, @animateEnterButton) + if @lastResponses + buttons = $('.enter button') + for response, i in @lastResponses + channel = response.channel.replace 'level-set-playing', 'level:set-playing' # Easier than migrating all those victory buttons. + f = (r) => => setTimeout((-> Backbone.Mediator.publish(channel, r.event or {})), 10) + $(buttons[i]).click(f(response)) + else + $('.enter', @bubble).click(-> Backbone.Mediator.publish('script:end-current-script', {})) + return + @animator.tick() + + onShiftSpacePressed: (e) -> + @shiftSpacePressed = (@shiftSpacePressed || 0) + 1 + # We don't need to handle script:end-current-script--that's done--but if we do have + # custom buttons, then we need to trigger the one that should fire (the last one). + # If we decide that always having the last one fire is bad, we should make it smarter. + return unless @lastResponses?.length + r = @lastResponses[@lastResponses.length - 1] + channel = r.channel.replace 'level-set-playing', 'level:set-playing' + _.delay (-> Backbone.Mediator.publish(channel, r.event or {})), 10 + + onEscapePressed: (e) -> + @escapePressed = true + + animateEnterButton: => + return unless @bubble + button = $('.enter', @bubble) + dot = $('.dot', button) + dot.animate({opacity: 0.2}, 300).animate({opacity: 1.9}, 600, @animateEnterButton) + + destroy: -> + clearInterval(@messageInterval) if @messageInterval + super() diff --git a/app/views/play/level/LevelHUDView.coffee b/app/views/play/level/LevelHUDView.coffee index 8dff83689..9235803f7 100644 --- a/app/views/play/level/LevelHUDView.coffee +++ b/app/views/play/level/LevelHUDView.coffee @@ -1,7 +1,6 @@ CocoView = require 'views/kinds/CocoView' template = require 'templates/play/level/hud' prop_template = require 'templates/play/level/hud_prop' -DialogueAnimator = require './DialogueAnimator' module.exports = class LevelHUDView extends CocoView id: 'thang-hud' @@ -180,6 +179,4 @@ module.exports = class LevelHUDView extends CocoView destroy: -> @stage?.stopTalking() - clearInterval(@messageInterval) if @messageInterval - clearTimeout @hintNextSelectionTimeout if @hintNextSelectionTimeout super() diff --git a/app/views/play/level/LevelPlaybackView.coffee b/app/views/play/level/LevelPlaybackView.coffee index 16e00516a..8fdb75af0 100644 --- a/app/views/play/level/LevelPlaybackView.coffee +++ b/app/views/play/level/LevelPlaybackView.coffee @@ -141,6 +141,7 @@ module.exports = class LevelPlaybackView extends CocoView console.warn('error disabling scrubber', error) @timePopup?.disable() $('#volume-button', @$el).removeClass('disabled') + @$el.addClass 'controls-disabled' onEnableControls: (e) -> return if @realTime @@ -152,6 +153,7 @@ module.exports = class LevelPlaybackView extends CocoView catch error console.warn('error enabling scrubber', error) @timePopup?.enable() + @$el.removeClass 'controls-disabled' onSetPlaying: (e) -> @playing = (e ? {}).playing ? true diff --git a/app/views/play/level/PlayLevelView.coffee b/app/views/play/level/PlayLevelView.coffee index 24fa86d1d..c44ab30fb 100644 --- a/app/views/play/level/PlayLevelView.coffee +++ b/app/views/play/level/PlayLevelView.coffee @@ -27,6 +27,7 @@ ProblemAlertView = require './tome/ProblemAlertView' TomeView = require './tome/TomeView' ChatView = require './LevelChatView' HUDView = require './LevelHUDView' +LevelDialogueView = require './LevelDialogueView' ControlBarView = require './ControlBarView' LevelPlaybackView = require './LevelPlaybackView' GoalsView = require './LevelGoalsView' @@ -246,6 +247,7 @@ module.exports = class PlayLevelView extends RootView @insertSubView new LevelFlagsView world: @world if (@levelID in ['sky-span', 'coinucopia']) or @level.get('type', true) in ['hero-ladder', 'hero-coop'] # TODO: figure out when flags are available @insertSubView new GoldView {} @insertSubView new HUDView {level: @level} + @insertSubView new LevelDialogueView {level: @level} @insertSubView new ChatView levelID: @levelID, sessionID: @session.id, session: @session if @level.get('type') in ['ladder', 'hero-ladder'] @insertSubView new MultiplayerStatusView levelID: @levelID, session: @session, level: @level