diff --git a/app/lib/DefaultScripts.coffee b/app/lib/DefaultScripts.coffee
index 8417ce978..818feee91 100644
--- a/app/lib/DefaultScripts.coffee
+++ b/app/lib/DefaultScripts.coffee
@@ -1,7 +1,20 @@
module.exports = [
{
- channel: "god:new-world-created"
- noteChain: []
id: "Introduction"
+ channel: "god:new-world-created"
+ noteChain: [
+ name: "Set camera, start music."
+ surface:
+ focus:
+ bounds: [{x: 0, y: 0}, {x: 80, y: 68}]
+ target: "Hero Placeholder"
+ zoom: 2
+ sound:
+ music:
+ file: "/music/music_level_2"
+ play: true
+ script:
+ duration: 1
+ ]
}
]
diff --git a/app/lib/God.coffee b/app/lib/God.coffee
index 8feafbfa1..cf1631689 100644
--- a/app/lib/God.coffee
+++ b/app/lib/God.coffee
@@ -131,9 +131,7 @@ module.exports = class God extends CocoClass
Backbone.Mediator.publish 'god:debug-world-load-progress-changed', progress: event.data.progress
onNewWorldCreated: (e) ->
- console.log 'filtering', _.cloneDeep e.world.userCodeMap
@currentUserCodeMap = @filterUserCodeMapWhenFromWorld e.world.userCodeMap
- console.log ' ... filtered into', _.cloneDeep e.world.userCodeMap
filterUserCodeMapWhenFromWorld: (worldUserCodeMap) ->
newUserCodeMap = {}
diff --git a/app/lib/surface/Dropper.coffee b/app/lib/surface/Dropper.coffee
index e7ea3c6cd..f074c702b 100644
--- a/app/lib/surface/Dropper.coffee
+++ b/app/lib/surface/Dropper.coffee
@@ -1,28 +1,27 @@
Dropper = class Dropper
- lost_frames: 0.0
- drop_counter: 0
+ lostFrames: 0.0
+ dropCounter: 0
constructor: ->
@listener = (e) => @tick(e)
tick: ->
unless @tickedOnce
- @tickedOnce = true # Can't get measured FPS on the 0th frame
+ @tickedOnce = true # Can't get measured FPS on the 0th frame.
return
- # decrement drop counter
- @drop_counter -= 1 if @drop_counter > 0
+ --@dropCounter if @dropCounter > 0
- # track number of frames we've lost since the last tick
+ # Track number of frames we've lost since the last tick.
fps = createjs.Ticker.getFPS()
actual = createjs.Ticker.getMeasuredFPS(1)
- @lost_frames += (fps - actual) / fps
+ @lostFrames += (fps - actual) / fps
- # if lost_frames > 1, drop that number for the next tick
- @drop_counter += parseInt(@lost_frames)
- @lost_frames = @lost_frames % 1
+ # If lostFrames > 1, drop that number for the next tick.
+ @dropCounter += parseInt @lostFrames
+ @lostFrames = @lostFrames % 1
drop: ->
- return @drop_counter > 0
+ return @dropCounter > 0
module.exports = new Dropper()
diff --git a/app/lib/surface/PlaybackOverScreen.coffee b/app/lib/surface/PlaybackOverScreen.coffee
index a50dadc78..49eca6f80 100644
--- a/app/lib/surface/PlaybackOverScreen.coffee
+++ b/app/lib/surface/PlaybackOverScreen.coffee
@@ -1,6 +1,9 @@
CocoClass = require 'lib/CocoClass'
module.exports = class PlaybackOverScreen extends CocoClass
+ subscriptions:
+ 'goal-manager:new-goal-states': 'onNewGoalStates'
+
constructor: (options) ->
super()
options ?= {}
@@ -16,15 +19,13 @@ module.exports = class PlaybackOverScreen extends CocoClass
@dimLayer = new createjs.Container()
@dimLayer.mouseEnabled = @dimLayer.mouseChildren = false
@dimLayer.addChild @dimScreen = new createjs.Shape()
- @dimScreen.graphics.beginFill('rgba(0,0,0,0.4)').rect 0, 0, @camera.canvasWidth, @camera.canvasHeight
- @dimLayer.cache 0, 0, @camera.canvasWidth, @camera.canvasHeight
@dimLayer.alpha = 0
@layer.addChild @dimLayer
show: ->
return if @showing
@showing = true
-
+ @updateColor 'rgba(212, 212, 212, 0.4)' unless @color # If we haven't caught the goal state for the first run, just do something neutral.
@dimLayer.alpha = 0
createjs.Tween.removeTweens @dimLayer
createjs.Tween.get(@dimLayer).to({alpha: 1}, 500)
@@ -32,6 +33,22 @@ module.exports = class PlaybackOverScreen extends CocoClass
hide: ->
return unless @showing
@showing = false
-
createjs.Tween.removeTweens @dimLayer
createjs.Tween.get(@dimLayer).to({alpha: 0}, 500)
+
+ onNewGoalStates: (e) ->
+ success = e.overallStatus is 'success'
+ failure = e.overallStatus is 'failure'
+ timedOut = e.timedOut
+ incomplete = not success and not failure and not timedOut
+ color = if failure then 'rgba(255, 128, 128, 0.4)' else 'rgba(255, 255, 255, 0.4)'
+ @updateColor color
+
+ updateColor: (color) ->
+ return if color is @color
+ @dimScreen.graphics.clear().beginFill(color).rect 0, 0, @camera.canvasWidth, @camera.canvasHeight
+ if @color
+ @dimLayer.updateCache()
+ else
+ @dimLayer.cache 0, 0, @camera.canvasWidth, @camera.canvasHeight # I wonder if caching is even worth it for just a rect fill.
+ @color = color
diff --git a/app/lib/surface/Surface.coffee b/app/lib/surface/Surface.coffee
index 980085db8..617dbd279 100644
--- a/app/lib/surface/Surface.coffee
+++ b/app/lib/surface/Surface.coffee
@@ -115,6 +115,7 @@ module.exports = Surface = class Surface extends CocoClass
@lankBoss = new LankBoss camera: @camera, webGLStage: @webGLStage, surfaceTextLayer: @surfaceTextLayer, world: @world, thangTypes: @options.thangTypes, choosing: @options.choosing, navigateToSelection: @options.navigateToSelection, showInvisible: @options.showInvisible
@countdownScreen = new CountdownScreen camera: @camera, layer: @screenLayer
@playbackOverScreen = new PlaybackOverScreen camera: @camera, layer: @screenLayer
+ @normalStage.addChildAt @playbackOverScreen.dimLayer, 0 # Put this below the other layers, actually, so we can more easily read text on the screen.
@waitingScreen = new WaitingScreen camera: @camera, layer: @screenLayer
@initCoordinates()
@webGLStage.enableMouseOver(10)
@@ -192,7 +193,7 @@ module.exports = Surface = class Surface extends CocoClass
@currentFrame = Math.min @currentFrame, lastFrame
newWorldFrame = Math.floor @currentFrame
if Dropper.drop()
- framesDropped += 1
+ ++framesDropped
else
worldFrameAdvanced = newWorldFrame isnt oldWorldFrame
if worldFrameAdvanced
@@ -231,8 +232,8 @@ module.exports = Surface = class Surface extends CocoClass
# world state must have been restored in @restoreWorldState
if @playing and @currentFrame < @world.frames.length - 1 and @heroLank and not @mouseIsDown and @camera.newTarget isnt @heroLank.sprite and @camera.target isnt @heroLank.sprite
@camera.zoomTo @heroLank.sprite, @camera.zoom, 750
- @camera.updateZoom()
@lankBoss.update frameChanged
+ @camera.updateZoom() # Make sure to do this right after the LankBoss updates, not before, so it can properly target sprite positions.
@dimmer?.setSprites @lankBoss.lanks
drawCurrentFrame: (e) ->
diff --git a/app/lib/world/world.coffee b/app/lib/world/world.coffee
index fb4ed80bd..90d1bd3fa 100644
--- a/app/lib/world/world.coffee
+++ b/app/lib/world/world.coffee
@@ -448,6 +448,8 @@ module.exports = class World
for thangID, methods of o.userCodeMap
for methodName, serializedAether of methods
for aetherStateKey in ['flow', 'metrics', 'style', 'problems']
+ w.userCodeMap[thangID] ?= {}
+ w.userCodeMap[thangID][methodName] ?= {}
w.userCodeMap[thangID][methodName][aetherStateKey] = serializedAether[aetherStateKey]
else
w = new World o.userCodeMap, classMap
diff --git a/app/styles/base.sass b/app/styles/base.sass
index e4e6ea228..10bef6efb 100644
--- a/app/styles/base.sass
+++ b/app/styles/base.sass
@@ -236,6 +236,17 @@ table.table
.ui-state-default, .ui-state-focus, .ui-state-active, .ui-state-highlight, .ui-state-error
background-image: none
+// DOM highlight pointer arrow
+
+.highlight-pointer
+ position: absolute
+ left: 0
+ top: 0
+ height: 100px
+ opacity: 0.0
+ pointer-events: none
+ z-index: 10
+
// Fonts
.header-font
diff --git a/app/styles/play/level.sass b/app/styles/play/level.sass
index 389453cbf..b4d5e0548 100644
--- a/app/styles/play/level.sass
+++ b/app/styles/play/level.sass
@@ -97,15 +97,6 @@ $level-resize-transition-time: 0.5s
bottom: 0
@include transition(width $level-resize-transition-time ease-in-out, right $level-resize-transition-time ease-in-out)
- #pointer
- position: absolute
- left: 0
- top: 0
- height: 100px
- opacity: 0.0
- pointer-events: none
- z-index: 10
-
// Level Docs
.ui-effects-transfer
border: 2px dotted gray
diff --git a/app/styles/play/level/goals.sass b/app/styles/play/level/goals.sass
index 39b5b53a8..9ce1381b2 100644
--- a/app/styles/play/level/goals.sass
+++ b/app/styles/play/level/goals.sass
@@ -5,19 +5,24 @@
position: absolute
left: 10px
top: -100px
- @include transition(top 0.5s ease-in-out)
+ @include transition(0.5s ease-in-out)
background-color: rgba(200,200,200,1.0)
border: black
padding: 15px 7px 2px 5px
box-sizing: border-box
- border: 1px solid #333
+ border: 1px solid #333
border-radius: 5px
-
+ z-index: 3
+ font-size: 14px
+
+ &.brighter
+ font-size: 28px
+
.goals-status
- font-size: 14px
margin: 0
color: black
+
.success
color: darkgreen
.timed-out
diff --git a/app/views/kinds/CocoView.coffee b/app/views/kinds/CocoView.coffee
index 18915a7da..3068f8015 100644
--- a/app/views/kinds/CocoView.coffee
+++ b/app/views/kinds/CocoView.coffee
@@ -59,6 +59,7 @@ module.exports = class CocoView extends Backbone.View
@undelegateEvents() # removes both events and subs
view.destroy() for id, view of @subviews
$('#modal-wrapper .modal').off 'hidden.bs.modal', @modalClosed
+ @endHighlight()
@[key] = undefined for key, value of @
@destroyed = true
@off = doNothing
@@ -293,6 +294,78 @@ module.exports = class CocoView extends Backbone.View
delete @subviews[view.parentKey]
view.destroy()
+ # Pointing stuff out
+
+ highlightElement: (selector, options) ->
+ @endHighlight()
+ options ?= {}
+ if delay = options.delay
+ delete options.delay
+ return @pointerDelayTimeout = _.delay((=> @highlightElement selector, options), delay)
+ $pointer = @getPointer()
+ $target = $(selector + ':visible')
+ return if parseFloat($target.css('opacity')) is 0.0 # Don't point out invisible elements.
+ return unless offset = $target.offset() # Don't point out elements we can't locate.
+ targetLeft = offset.left + $target.outerWidth() * 0.5
+ targetTop = offset.top + $target.outerHeight() * 0.5
+
+ if options.sides
+ if 'left' in options.sides then targetLeft = offset.left
+ if 'right' in options.sides then targetLeft = offset.left + $target.outerWidth()
+ if 'top' in options.sides then targetTop = offset.top
+ if 'bottom' in options.sides then targetTop = offset.top + $target.outerHeight()
+ else
+ # Aim to hit the side if the target is entirely on one side of the screen.
+ if offset.left > @$el.outerWidth() * 0.5
+ targetLeft = offset.left
+ else if offset.left + $target.outerWidth() < @$el.outerWidth() * 0.5
+ targetLeft = offset.left + $target.outerWidth()
+
+ # Aim to hit the bottom or top if the target is entirely on the top or bottom of the screen.
+ if offset.top > @$el.outerWidth() * 0.5
+ targetTop = offset.top
+ else if offset.top + $target.outerHeight() < @$el.outerHeight() * 0.5
+ targetTop = offset.top + $target.outerHeight()
+
+ if options.offset
+ targetLeft += options.offset.x
+ targetTop += options.offset.y
+
+ @pointerRadialDistance = -47
+ @pointerRotation = options.rotation ? Math.atan2(@$el.outerWidth() * 0.5 - targetLeft, targetTop - @$el.outerHeight() * 0.5)
+ $pointer.css
+ opacity: 1.0
+ transition: 'none'
+ transform: "rotate(#{@pointerRotation}rad) translate(-3px, #{@pointerRadialDistance}px)"
+ top: targetTop - 50
+ left: targetLeft - 50
+ _.defer =>
+ return if @destroyed
+ @animatePointer()
+ clearInterval @pointerInterval
+ @pointerInterval = setInterval(@animatePointer, 1200)
+ if options.duration
+ @pointerDurationTimeout = _.delay (=> @endHighlight() unless @destroyed), options.duration
+
+ animatePointer: =>
+ $pointer = @getPointer()
+ $pointer.css transition: 'all 0.6s ease-out', transform: "rotate(#{@pointerRotation}rad) translate(-3px, #{@pointerRadialDistance-50}px)"
+ #Backbone.Mediator.publish 'audio-player:play-sound', trigger: 'dom_highlight', volume: 0.75 # Never mind, this is currently so annoying
+ setTimeout (=> $pointer.css transition: 'all 0.4s ease-in', transform: "rotate(#{@pointerRotation}rad) translate(-3px, #{@pointerRadialDistance}px)"), 800
+
+ endHighlight: ->
+ @getPointer().css('opacity', 0.0)
+ clearInterval @pointerInterval
+ clearTimeout @pointerDelayTimeout
+ clearTimeout @pointerDurationTimeout
+ @pointerInterval = @pointerDelayTimeout = @pointerDurationTimeout = null
+
+ getPointer: ->
+ return $pointer if ($pointer = @$el.find('.highlight-pointer')) and $pointer.length
+ $pointer = $('')
+ @$el.append($pointer)
+ $pointer
+
# Utilities
getQueryVariable: (param, defaultValue) -> CocoView.getQueryVariable(param, defaultValue)
diff --git a/app/views/play/WorldMapView.coffee b/app/views/play/WorldMapView.coffee
index 43bac56ce..7b63c53e9 100644
--- a/app/views/play/WorldMapView.coffee
+++ b/app/views/play/WorldMapView.coffee
@@ -89,6 +89,7 @@ module.exports = class WorldMapView extends RootView
@$el.find('.level').tooltip()
@$el.addClass _.string.slugify @terrain
@updateVolume()
+ @highlightElement '.level.next', delay: 8000, duration: 20000, rotation: 0, sides: ['top']
onSessionsLoaded: (e) ->
for session in @sessions.models
diff --git a/app/views/play/level/LevelGoalsView.coffee b/app/views/play/level/LevelGoalsView.coffee
index 27849d659..04000c569 100644
--- a/app/views/play/level/LevelGoalsView.coffee
+++ b/app/views/play/level/LevelGoalsView.coffee
@@ -4,9 +4,9 @@ template = require 'templates/play/level/goals'
utils = require 'lib/utils'
stateIconMap =
- incomplete: 'icon-minus'
- success: 'icon-ok'
- failure: 'icon-remove'
+ incomplete: 'glyphicon-minus'
+ success: 'glyphicon-ok'
+ failure: 'glyphicon-remove'
module.exports = class LevelGoalsView extends CocoView
id: 'goals-view'
@@ -61,32 +61,47 @@ module.exports = class LevelGoalsView extends CocoView
# This should really get refactored, along with GoalManager, so that goals have a standard
# representation of how many are done, how many are needed, what that means, etc.
li = $('