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