diff --git a/app/templates/play/spectate.jade b/app/templates/play/spectate.jade
new file mode 100644
index 000000000..9be37b46a
--- /dev/null
+++ b/app/templates/play/spectate.jade
@@ -0,0 +1,16 @@
+.level-content
+ #control-bar-view
+
+ #canvas-wrapper
+ canvas(width=924, height=589)#surface
+ #canvas-left-gradient.gradient
+ #canvas-top-gradient.gradient
+ #goals-view.hide
+ #gold-view.hide.expanded
+ #level-chat-view
+ #playback-view
+ #thang-hud
+.footer
+ .content
+ p(class='footer-link-text')
+ a(title='Contact', tabindex=-1, data-toggle="coco-modal", data-target="modal/contact", data-i18n="nav.contact") Contact
diff --git a/app/views/play/spectate_view.coffee b/app/views/play/spectate_view.coffee
new file mode 100644
index 000000000..8cc361a43
--- /dev/null
+++ b/app/views/play/spectate_view.coffee
@@ -0,0 +1,336 @@
+View = require 'views/kinds/RootView'
+template = require 'templates/play/spectate'
+{me} = require('lib/auth')
+ThangType = require 'models/ThangType'
+
+# temp hard coded data
+World = require 'lib/world/world'
+docs = require 'lib/world/docs'
+
+# tools
+Surface = require 'lib/surface/Surface'
+God = require 'lib/God'
+GoalManager = require 'lib/world/GoalManager'
+ScriptManager = require 'lib/scripts/ScriptManager'
+LevelBus = require('lib/LevelBus')
+LevelLoader = require 'lib/LevelLoader'
+LevelSession = require 'models/LevelSession'
+Level = require 'models/Level'
+LevelComponent = require 'models/LevelComponent'
+Camera = require 'lib/surface/Camera'
+
+# subviews
+TomeView = require './level/tome/tome_view'
+ChatView = require './level/level_chat_view'
+HUDView = require './level/hud_view'
+ControlBarView = require './level/control_bar_view'
+PlaybackView = require './level/playback_view'
+GoalsView = require './level/goals_view'
+GoldView = require './level/gold_view'
+VictoryModal = require './level/modal/victory_modal'
+InfiniteLoopModal = require './level/modal/infinite_loop_modal'
+
+LoadingScreen = require 'lib/LoadingScreen'
+
+PROFILE_ME = false
+
+PlayLevelView = require './level_view'
+
+module.exports = class SpectateLevelView extends View
+ id: 'spectate-level-view'
+ template: template
+ cache: false
+ shortcutsEnabled: true
+ startsLoading: true
+ isEditorPreview: false
+
+ subscriptions:
+ 'level-set-volume': (e) -> createjs.Sound.setVolume(e.volume)
+ 'level-highlight-dom': 'onHighlightDom'
+ 'end-level-highlight-dom': 'onEndHighlight'
+ 'level-focus-dom': 'onFocusDom'
+ 'level-disable-controls': 'onDisableControls'
+ 'level-enable-controls': 'onEnableControls'
+ 'god:new-world-created': 'onNewWorld'
+ 'god:infinite-loop': 'onInfiniteLoop'
+ 'edit-wizard-settings': 'showWizardSettingsModal'
+ 'surface:world-set-up': 'onSurfaceSetUpNewWorld'
+ 'level:session-will-save': 'onSessionWillSave'
+ 'level:set-team': 'setTeam'
+
+ events:
+ 'click #level-done-button': 'onDonePressed'
+
+
+ constructor: (options, @levelID) ->
+ console.profile?() if PROFILE_ME
+ super options
+ console.log @levelID
+
+ @ogreSessionID = @getQueryVariable 'ogres'
+ @humanSessionID = @getQueryVariable 'humans'
+
+
+ $(window).on('resize', @onWindowResize)
+ @supermodel.once 'error', =>
+ msg = $.i18n.t('play_level.level_load_error', defaultValue: "Level could not be loaded.")
+ @$el.html('
' + msg + '
')
+
+
+ @load()
+
+
+
+ setLevel: (@level, @supermodel) ->
+ @god?.level = @level.serialize @supermodel
+ if @world
+ serializedLevel = @level.serialize(@supermodel)
+ @world.loadFromLevel serializedLevel, false
+ else
+ @load()
+
+ load: ->
+ @levelLoader = new LevelLoader(@levelID, @supermodel, @sessionID)
+ @levelLoader.once 'loaded-all', @onLevelLoaderLoaded
+ @god = new God()
+
+ getRenderData: ->
+ c = super()
+ c.world = @world
+ c
+
+ afterRender: ->
+ window.onPlayLevelViewLoaded? @ # still a hack
+ @loadingScreen = new LoadingScreen(@$el.find('canvas')[0])
+ @loadingScreen.show()
+ super()
+
+ onLevelLoaderLoaded: =>
+ #needs editing
+ @session = @levelLoader.session
+ @world = @levelLoader.world
+ @level = @levelLoader.level
+ @levelLoader.destroy()
+ @levelLoader = null
+ @loadingScreen.destroy()
+ @god.level = @level.serialize @supermodel
+ @god.worldClassMap = @world.classMap
+ #@setTeam @world.teamForPlayer _.size @session.get 'players' # TODO: players aren't initialized yet?
+ @setTeam @getQueryVariable("team") ? @world.teamForPlayer(0)
+ @initSurface()
+ @initGoalManager()
+ @initScriptManager()
+ @insertSubviews()
+ @initVolume()
+ @session.on 'change:multiplayer', @onMultiplayerChanged, @
+ @originalSessionState = _.cloneDeep(@session.get('state'))
+ @register()
+ @controlBar.setBus(@bus)
+ @surface.showLevel()
+
+ onSupermodelLoadedOne: =>
+ @modelsLoaded ?= 0
+ @modelsLoaded += 1
+ @updateInitString()
+
+ updateInitString: ->
+ return if @surface
+ @modelsLoaded ?= 0
+ canvas = @$el.find('#surface')[0]
+ ctx = canvas.getContext('2d')
+ ctx.font="20px Georgia"
+ ctx.clearRect(0, 0, canvas.width, canvas.height)
+ ctx.fillText("Loaded #{@modelsLoaded} thingies",50,50)
+
+ insertSubviews: ->
+ #needs editing
+ @insertSubView @tome = new TomeView levelID: @levelID, session: @session, thangs: @world.thangs, supermodel: @supermodel
+ @insertSubView new PlaybackView {}
+ @insertSubView new GoalsView {}
+ @insertSubView new GoldView {}
+ @insertSubView new HUDView {}
+ @insertSubView new ChatView levelID: @levelID, sessionID: @session.id, session: @session
+ worldName = @level.get('i18n')?[me.lang()]?.name ? @level.get('name')
+ @controlBar = @insertSubView new ControlBarView {worldName: worldName, session: @session, level: @level, supermodel: @supermodel, playableTeams: @world.playableTeams}
+ #Backbone.Mediator.publish('level-set-debug', debug: true) if me.displayName() is 'Nick!'
+
+ afterInsert: ->
+ super()
+
+
+ onWindowResize: (s...) ->
+ $('#pointer').css('opacity', 0.0)
+
+ onDisableControls: (e) =>
+ return if e.controls and not ('level' in e.controls)
+ @shortcutsEnabled = false
+ @wasFocusedOn = document.activeElement
+ $('body').focus()
+
+ onEnableControls: (e) =>
+ return if e.controls? and not ('level' in e.controls)
+ @shortcutsEnabled = true
+ $(@wasFocusedOn).focus() if @wasFocusedOn
+ @wasFocusedOn = null
+
+ onDonePressed: => @showVictory()
+
+ onNewWorld: (e) ->
+ @world = e.world
+
+ onInfiniteLoop: (e) ->
+ return unless e.firstWorld
+ @openModalView new InfiniteLoopModal()
+ window.tracker?.trackEvent 'Saw Initial Infinite Loop', level: @world.name, label: @world.name
+
+
+ getNextLevel: ->
+ nextLevelOriginal = @level.get('nextLevel')?.original
+ levels = @supermodel.getModels(Level)
+ return l for l in levels when l.get('original') is nextLevelOriginal
+
+ onHighlightDom: (e) =>
+ if e.delay
+ delay = e.delay
+ delete e.delay
+ @pointerInterval = _.delay((=> @onHighlightDom e), delay)
+ return
+ @addPointer()
+ selector = e.selector + ':visible'
+ dom = $(selector)
+ return if parseFloat(dom.css('opacity')) is 0.0
+ offset = dom.offset()
+ return if not offset
+ target_left = offset.left + dom.outerWidth() * 0.5
+ target_top = offset.top + dom.outerHeight() * 0.5
+ body = $('#level-view')
+
+ if e.sides
+ if 'left' in e.sides then target_left = offset.left
+ if 'right' in e.sides then target_left = offset.left + dom.outerWidth()
+ if 'top' in e.sides then target_top = offset.top
+ if 'bottom' in e.sides then target_top = offset.top + dom.outerHeight()
+ else
+ # aim to hit the side if the target is entirely on one side of the screen
+ if offset.left > body.outerWidth()*0.5
+ target_left = offset.left
+ else if offset.left + dom.outerWidth() < body.outerWidth()*0.5
+ target_left = offset.left + dom.outerWidth()
+
+ # aim to hit the bottom or top if the target is entirely on the top or bottom of the screen
+ if offset.top > body.outerWidth()*0.5
+ target_top = offset.top
+ else if offset.top + dom.outerHeight() < body.outerHeight()*0.5
+ target_top = offset.top + dom.outerHeight()
+
+ if e.offset
+ target_left += e.offset.x
+ target_top += e.offset.y
+
+ @pointerRadialDistance = -47 # - Math.sqrt(Math.pow(dom.outerHeight()*0.5, 2), Math.pow(dom.outerWidth()*0.5))
+ @pointerRotation = e.rotation ? Math.atan2(body.outerWidth()*0.5 - target_left, target_top - body.outerHeight()*0.5)
+ pointer = $('#pointer')
+ pointer
+ .css('opacity', 1.0)
+ .css('transition', 'none')
+ .css('transform', "rotate(#{@pointerRotation}rad) translate(-3px, #{@pointerRadialDistance}px)")
+ .css('top', target_top - 50)
+ .css('left', target_left - 50)
+ setTimeout((=>
+ @animatePointer()
+ clearInterval(@pointerInterval)
+ @pointerInterval = setInterval(@animatePointer, 1200)
+ ), 1)
+
+
+ animatePointer: =>
+ pointer = $('#pointer')
+ pointer.css('transition', 'all 0.6s ease-out')
+ pointer.css('transform', "rotate(#{@pointerRotation}rad) translate(-3px, #{@pointerRadialDistance-50}px)")
+ setTimeout((=>
+ pointer.css('transform', "rotate(#{@pointerRotation}rad) translate(-3px, #{@pointerRadialDistance}px)").css('transition', 'all 0.4s ease-in')), 800)
+
+ onFocusDom: (e) => $(e.selector).focus()
+
+ onEndHighlight: =>
+ $('#pointer').css('opacity', 0.0)
+ clearInterval(@pointerInterval)
+
+
+ # initialization
+
+ addPointer: ->
+ p = $('#pointer')
+ return if p.length
+ @$el.append($(''))
+
+ initSurface: ->
+ surfaceCanvas = $('canvas#surface', @$el)
+ @surface = new Surface(@world, surfaceCanvas, thangTypes: @supermodel.getModels(ThangType), playJingle: not @isEditorPreview)
+ worldBounds = @world.getBounds()
+ bounds = [{x:worldBounds.left, y:worldBounds.top}, {x:worldBounds.right, y:worldBounds.bottom}]
+ @surface.camera.setBounds(bounds)
+ @surface.camera.zoomTo({x:0, y:0}, 0.1, 0)
+
+ initGoalManager: ->
+ @goalManager = new GoalManager(@world)
+ @god.goalManager = @goalManager
+
+ initScriptManager: ->
+ @scriptManager = new ScriptManager({scripts: @world.scripts or [], view:@, session: @session})
+ @scriptManager.loadFromSession()
+
+ initVolume: ->
+ volume = me.get('volume')
+ volume = 1.0 unless volume?
+ Backbone.Mediator.publish 'level-set-volume', volume: volume
+
+ onSurfaceSetUpNewWorld: ->
+ return if @alreadyLoadedState
+ @alreadyLoadedState = true
+ state = @originalSessionState
+ if state.frame
+ Backbone.Mediator.publish 'level-set-time', { time: 0, frameOffset: state.frame }
+ if state.selected
+ # TODO: Should also restore selected spell here by saving spellName
+ Backbone.Mediator.publish 'level-select-sprite', { thangID: state.selected, spellName: null }
+ if state.playing?
+ Backbone.Mediator.publish 'level-set-playing', { playing: state.playing }
+
+
+ register: ->
+ @bus = LevelBus.get(@levelID, @session.id)
+ @bus.setSession(@session)
+ @bus.setTeamSpellMap @tome.teamSpellMap
+ @bus.connect() if @session.get('multiplayer')
+
+ onSessionWillSave: (e) ->
+ # Something interesting has happened, so (at a lower frequency), we'll save a screenshot.
+ @saveScreenshot e.session
+
+ # Throttled
+ saveScreenshot: (session) =>
+ return unless screenshot = @surface?.screenshot()
+ session.save {screenshot: screenshot}, {patch: true}
+
+ setTeam: (team) ->
+ team = team?.team unless _.isString team
+ team ?= 'humans'
+ me.team = team
+ Backbone.Mediator.publish 'level:team-set', team: team
+
+ destroy: ->
+ super()
+ @levelLoader?.destroy()
+ @surface?.destroy()
+ @god?.destroy()
+ @goalManager?.destroy()
+ @scriptManager?.destroy()
+ $(window).off('resize', @onWindowResize)
+ delete window.world # not sure where this is set, but this is one way to clean it up
+
+ clearInterval(@pointerInterval)
+ @bus?.destroy()
+ #@instance.save() unless @instance.loading
+ console.profileEnd?() if PROFILE_ME
+ @session.off 'change:multiplayer', @onMultiplayerChanged, @
diff --git a/server/levels/sessions/level_session_handler.coffee b/server/levels/sessions/level_session_handler.coffee
index 4996b8e53..1b9f8a241 100644
--- a/server/levels/sessions/level_session_handler.coffee
+++ b/server/levels/sessions/level_session_handler.coffee
@@ -7,7 +7,7 @@ class LevelSessionHandler extends Handler
modelClass: LevelSession
editableProperties: ['multiplayer', 'players', 'code', 'completed', 'state',
'levelName', 'creatorName', 'levelID', 'screenshot',
- 'chat', 'teamSpells']
+ 'chat', 'teamSpells','submitted']
getByRelationship: (req, res, args...) ->
return @sendNotFoundError(res) unless args.length is 2 and args[1] is 'active'
diff --git a/server/levels/sessions/level_session_schema.coffee b/server/levels/sessions/level_session_schema.coffee
index 3e6c9f007..8f1a6beaa 100644
--- a/server/levels/sessions/level_session_schema.coffee
+++ b/server/levels/sessions/level_session_schema.coffee
@@ -67,7 +67,7 @@ _.extend LevelSessionSchema.properties,
meanStrength: {type: 'number', default: 25}
standardDeviation: {type:'number', default:25/3, minimum: 0}
totalScore: {type: 'number', default: 10}
-
+ submitted: {type: 'boolean', default: false, index:true}
c.extendBasicProperties LevelSessionSchema, 'level.session'
c.extendPermissionsProperties LevelSessionSchema, 'level.session'
diff --git a/server/queues/scoring.coffee b/server/queues/scoring.coffee
index ccca45d5c..f8fd841d1 100644
--- a/server/queues/scoring.coffee
+++ b/server/queues/scoring.coffee
@@ -29,13 +29,30 @@ throwScoringQueueRegistrationError = (error) ->
throw new Error "There was an error registering the scoring queue."
module.exports.createNewTask = (req, res) ->
- scoringTaskQueue.sendMessage req.body, 0, (err, data) ->
- return errors.badInput res, "There was an error creating the message, reason: #{err}" if err?
+ return errors.badInput res, "The session ID is invalid" unless typeof req.body.session is "string"
+ LevelSession.findOne { "_id": req.body.session}, (err, sessionToScore) ->
+ return errors.serverError res, "There was an error finding the given session." if err?
- res.send data
- res.end()
+ sessionToScore.submitted = true
+ LevelSession.update { "_id": req.body.session}, {"submitted":true}, (err, data) ->
+ return errors.serverError res, "There was an error saving the submitted bool of the session." if err?
+ LevelSession.find { "levelID": "project-dota", "submitted": true}, (err, submittedSessions) ->
+ taskPairs = []
+ for session in submittedSessions
+ if String(session._id) isnt req.body.session
+ taskPairs.push [req.body.session,String session._id]
+ async.each taskPairs, sendTaskPairToQueue, (taskPairError) ->
+ return errors.serverError res, "There was an error sending the task pairs to the queue" if taskPairError?
+ sendResponseObject req, res, {"message":"All task pairs were succesfully sent to the queue"}
+sendTaskPairToQueue = (taskPair, callback) ->
+ taskObject =
+ sessions: taskPair
+
+ scoringTaskQueue.sendMessage taskObject, 0, (err,data) ->
+ callback err,data
+
module.exports.dispatchTaskToConsumer = (req, res) ->
userID = getUserIDFromRequest req,res
return errors.forbidden res, "You need to be logged in to simulate games" if isUserAnonymous req