Adding easy observing of leaderboard matches (except top 5).

This commit is contained in:
Nick Winter 2015-01-31 10:04:02 -08:00
parent c5e47fda50
commit c977ecc16f
15 changed files with 91 additions and 53 deletions

View file

@ -88,6 +88,8 @@ module.exports = class LevelLoader extends CocoClass
@listenToOnce @opponentSession, 'sync', @loadDependenciesForSession
loadDependenciesForSession: (session) ->
if me.id isnt session.get 'creator'
session.patch = session.save = -> console.error "Not saving session, since we didn't create it."
if session is @session
codeLanguage = session.get('codeLanguage') or me.get('aceConfig')?.language or 'python'
modulePath = "vendor/aether-#{codeLanguage}"
@ -136,6 +138,7 @@ module.exports = class LevelLoader extends CocoClass
@onWorldNecessitiesLoaded()
addSessionBrowserInfo: (session) ->
return unless me.id is session.get 'creator'
return unless $.browser?
browser = {}
browser['desktop'] = $.browser.desktop if $.browser.desktop

View file

@ -342,7 +342,7 @@ _.extend LevelSchema.properties,
type: 'string', links: [{rel: 'db', href: '/db/thang.type/{($)}/version'}], format: 'latest-version-original-reference'
}}
campaign: c.shortString title: 'Campaign', description: 'Which campaign this level is part of (like "desert").', format: 'hidden' # Automatically set by campaign editor.
scoreTypes: c.array {title: 'Score Types', description: 'What metric to show leaderboards for.'},
scoreTypes: c.array {title: 'Score Types', description: 'What metric to show leaderboards for.', uniqueItems: true},
c.shortString(title: 'Score Type', 'enum': ['time', 'damage-taken', 'damage-dealt', 'gold-collected', 'difficulty']) # TODO: good version of LoC; total gear value.

View file

@ -16,6 +16,10 @@
th
text-align: center
tbody
tr.viewable
cursor: pointer
.rank-cell
font-weight: bold
@ -27,6 +31,6 @@
.hero-portrait-cell, .code-language-cell
background: transparent url(/images/common/code_languages/javascript_small.png) no-repeat center center
padding: 0 9px
background-size: 30px 30px
height: 30px
width: 32px

View file

@ -9,7 +9,7 @@
.glyphicon.glyphicon-play
span(data-i18n="nav.play").home-text Levels
if isMultiplayerLevel
if isMultiplayerLevel && !observing
.multiplayer-area-container
.multiplayer-area
.multiplayer-label(data-i18n="play_level.control_bar_multiplayer")
@ -28,6 +28,7 @@ else
.buttons-area
if !observing
button.btn.btn-inverse#game-menu-button(title="Show game menu")
.hamburger
span.icon-bar
@ -38,6 +39,7 @@ else
if spectateGame
button.btn.btn-xs.btn-inverse.banner#next-game-button(title="Next Game", data-i18n="play_level.next-game") Next game!
if !observing
button.btn.btn-xs.btn-primary.banner#level-done-button(data-i18n="play_level.done") Done
if me.get('anonymous')

View file

@ -1,6 +1,7 @@
button.btn.btn-lg.btn-illustrated.cast-button(title=castVerbose)
span(data-i18n="play_level.tome_run_button_ran") Ran
if !observing
button.btn.btn-lg.btn-illustrated.submit-button(title=castRealTimeVerbose)
span(data-i18n="play_level.tome_submit_button") Submit
span.spl.secret.submit-again-time

View file

@ -11,16 +11,23 @@ if topScores
th(colspan=4, data-i18n="general.player")
th(data-i18n="general.score")
th(data-i18n="general.when")
th
tbody
for row, rank in topScores
- var isMyRow = row.creator == me.id
tr(class=isMyRow ? "success" : "", data-player-id=row.creator, data-session-id=row.id)
- var viewable = rank >= 5 || me.isAdmin();
tr(class=isMyRow ? "success" : "" + (viewable ? " viewable" : ""), data-player-id=row.creator, data-session-id=row.session, title=viewable ? "View solution" : "Can't view top 5 solutions")
td.rank-cell= rank + 1
td.code-language-cell(style="background-image: url(/images/common/code_languages/#{row.codeLanguage}_small.png)" title=_.string.capitalize(row.codeLanguage))
td.hero-portrait-cell(style="background-image: url(/file/db/thang.type/#{row.hero}/portrait.png)")
td.name-col-cell= row.creatorName || "Anonymous"
td.score-cell= row.score
td.ago-cell= row.ago
td.viewable-cell
if viewable
.glyphicon.glyphicon-eye-open
else
.glyphicon.glyphicon-eye-close
else if loading
h3(data-i18n="common.loading")
else

View file

@ -206,7 +206,7 @@ module.exports = class CampaignView extends RootView
for nextLevelOriginal in level.nextLevels ? []
if nextLevel = _.find(@campaign.renderedLevels, original: nextLevelOriginal)
@createLine level.position, nextLevel.position
@showLeaderboard @options.justBeatLevel?.get('slug') if @options.showLeaderboard #or true
@showLeaderboard @options.justBeatLevel?.get('slug') if @options.showLeaderboard or true
@applyCampaignStyles()
@testParticles()
@ -220,7 +220,7 @@ module.exports = class CampaignView extends RootView
@openModalView authModal
showLeaderboard: (levelSlug) ->
#levelSlug ?= 'keeping-time' # Testing: show Keeping Time
levelSlug ?= 'siege-of-stonehold' # Testing
leaderboardModal = new LeaderboardModal supermodel: @supermodel, levelSlug: levelSlug
@openModalView leaderboardModal

View file

@ -33,6 +33,7 @@ module.exports = class ControlBarView extends CocoView
@level = options.level
@levelID = @level.get('slug')
@spectateGame = options.spectateGame ? false
@observing = options.session.get('creator') isnt me.id
super options
if @level.get('type') in ['hero-ladder'] and me.isAdmin()
@isMultiplayerLevel = true
@ -66,6 +67,7 @@ module.exports = class ControlBarView extends CocoView
c.difficultyTitle = "#{$.i18n.t 'play.level_difficulty'}#{c.levelDifficulty}"
@lastDifficulty = c.levelDifficulty
c.spectateGame = @spectateGame
c.observing = @observing
@homeViewArgs = [{supermodel: if @hasReceivedMemoryWarning then null else @supermodel}]
if @level.get('type', true) in ['ladder', 'ladder-tutorial', 'hero-ladder']
levelID = @level.get('slug').replace /\-tutorial$/, ''

View file

@ -95,6 +95,7 @@ module.exports = class PlayLevelView extends RootView
@isEditorPreview = @getQueryVariable 'dev'
@sessionID = @getQueryVariable 'session'
@observing = @getQueryVariable 'observing'
@opponentSessionID = @getQueryVariable('opponent')
@opponentSessionID ?= @options.opponent
@ -109,7 +110,7 @@ module.exports = class PlayLevelView extends RootView
setTimeout f, 100
else
@load()
application.tracker?.trackEvent 'Started Level Load', category: 'Play Level', level: @levelID, label: @levelID, ['Google Analytics']
application.tracker?.trackEvent 'Started Level Load', category: 'Play Level', level: @levelID, label: @levelID, ['Google Analytics'] unless @observing
setLevel: (@level, givenSupermodel) ->
@supermodel.models = givenSupermodel.models
@ -134,6 +135,7 @@ module.exports = class PlayLevelView extends RootView
@loadEndTime = new Date()
loadDuration = @loadEndTime - @loadStartTime
console.debug "Level unveiled after #{(loadDuration / 1000).toFixed(2)}s"
unless @observing
application.tracker?.trackEvent 'Finished Level Load', category: 'Play Level', label: @levelID, level: @levelID, loadDuration: loadDuration, ['Google Analytics']
application.tracker?.trackTiming loadDuration, 'Level Load Time', @levelID, @levelID
@ -147,7 +149,7 @@ module.exports = class PlayLevelView extends RootView
afterRender: ->
super()
window.onPlayLevelViewLoaded? @ # still a hack
@insertSubView @loadingView = new LevelLoadingView autoUnveil: @options.autoUnveil, level: @levelLoader?.level ? @level # May not have @level loaded yet
@insertSubView @loadingView = new LevelLoadingView autoUnveil: @options.autoUnveil or @observing, level: @levelLoader?.level ? @level # May not have @level loaded yet
@$el.find('#level-done-button').hide()
$('body').addClass('is-playing')
$('body').bind('touchmove', false) if @isIPadApp()
@ -233,7 +235,7 @@ module.exports = class PlayLevelView extends RootView
@god.setGoalManager @goalManager
insertSubviews: ->
@insertSubView @tome = new TomeView levelID: @levelID, session: @session, otherSession: @otherSession, thangs: @world.thangs, supermodel: @supermodel, level: @level
@insertSubView @tome = new TomeView levelID: @levelID, session: @session, otherSession: @otherSession, thangs: @world.thangs, supermodel: @supermodel, level: @level, observing: @observing
@insertSubView new LevelPlaybackView session: @session, level: @level
@insertSubView new GoalsView {}
@insertSubView new LevelFlagsView levelID: @levelID, world: @world if @$el.hasClass 'flags'
@ -283,7 +285,7 @@ module.exports = class PlayLevelView extends RootView
return unless @levelLoader.progress() is 1 # double check, since closing the guide may trigger this early
# Save latest level played.
if not (@levelLoader.level.get('type') in ['ladder', 'ladder-tutorial'])
if not @observing and not (@levelLoader.level.get('type') in ['ladder', 'ladder-tutorial'])
me.set('lastLevel', @levelID)
me.save()
application.tracker?.identify()
@ -321,7 +323,7 @@ module.exports = class PlayLevelView extends RootView
if @otherSession and not (@level.get('type', true) in ['hero', 'hero-ladder', 'hero-coop'])
# TODO: colorize name and cloud by team, colorize wizard by user's color config
@surface.createOpponentWizard id: @otherSession.get('creator'), name: @otherSession.get('creatorName'), team: @otherSession.get('team'), levelSlug: @level.get('slug'), codeLanguage: @otherSession.get('submittedCodeLanguage')
if @isEditorPreview
if @isEditorPreview or @observing
@loadingView.startUnveiling()
@loadingView.unveil()
@ -337,7 +339,7 @@ module.exports = class PlayLevelView extends RootView
Backbone.Mediator.publish 'playback:real-time-playback-waiting', {}
@realTimeMultiplayerContinueGame @options.realTimeMultiplayerSessionID
# TODO: Is it possible to create a Mongoose ObjectId for 'ls', instead of the string returned from get()?
application.tracker?.trackEvent 'Started Level', category:'Play Level', levelID: @levelID, ls: @session?.get('_id')
application.tracker?.trackEvent 'Started Level', category:'Play Level', levelID: @levelID, ls: @session?.get('_id') unless @observing
playAmbientSound: ->
return if @destroyed
@ -414,7 +416,7 @@ module.exports = class PlayLevelView extends RootView
return if @victorySeen
@victorySeen = true
victoryTime = (new Date()) - @loadEndTime
if victoryTime > 10 * 1000 # Don't track it if we're reloading an already-beaten level
if not @observing and victoryTime > 10 * 1000 # Don't track it if we're reloading an already-beaten level
application.tracker?.trackEvent 'Saw Victory',
category: 'Play Level'
level: @level.get('name')
@ -436,12 +438,12 @@ module.exports = class PlayLevelView extends RootView
@tome.reloadAllCode()
Backbone.Mediator.publish 'level:restarted', {}
$('#level-done-button', @$el).hide()
application.tracker?.trackEvent 'Confirmed Restart', category: 'Play Level', level: @level.get('name'), label: @level.get('name')
application.tracker?.trackEvent 'Confirmed Restart', category: 'Play Level', level: @level.get('name'), label: @level.get('name') unless @observing
onInfiniteLoop: (e) ->
return unless e.firstWorld
@openModalView new InfiniteLoopModal()
application.tracker?.trackEvent 'Saw Initial Infinite Loop', category: 'Play Level', level: @level.get('name'), label: @level.get('name')
application.tracker?.trackEvent 'Saw Initial Infinite Loop', category: 'Play Level', level: @level.get('name'), label: @level.get('name') unless @observing
onHighlightDOM: (e) -> @highlightElement e.selector, delay: e.delay, sides: e.sides, offset: e.offset, rotation: e.rotation

View file

@ -27,6 +27,7 @@ module.exports = class CastButtonView extends CocoView
@spells = options.spells
@castShortcut = '⇧↵'
@updateReplayabilityInterval = setInterval @updateReplayability, 1000
@observing = options.session.get('creator') isnt me.id
destroy: ->
clearInterval @updateReplayabilityInterval
@ -40,6 +41,7 @@ module.exports = class CastButtonView extends CocoView
castRealTimeShortcutVerbose = (if @isMac() then 'Cmd' else 'Ctrl') + '+' + castShortcutVerbose
context.castVerbose = castShortcutVerbose + ': ' + $.i18n.t('keyboard_shortcuts.run_code')
context.castRealTimeVerbose = castRealTimeShortcutVerbose + ': ' + $.i18n.t('keyboard_shortcuts.run_real_time')
context.observing = @observing
context
afterRender: ->

View file

@ -15,6 +15,7 @@ module.exports = class Spell
@otherSession = options.otherSession
@spectateView = options.spectateView
@spectateOpponentCodeLanguage = options.spectateOpponentCodeLanguage
@observing = options.observing
@supermodel = options.supermodel
@skipProtectAPI = options.skipProtectAPI
@worker = options.worker
@ -189,6 +190,7 @@ module.exports = class Spell
return true if @spectateView # Use transpiled code for both teams if we're just spectating.
return true if @isEnemySpell() # Use transpiled for enemy spells.
# Players without permissions can't view the raw code.
return false if @observing and @levelType is 'hero'
return true if @session.get('creator') isnt me.id and not (me.isAdmin() or 'employer' in me.get('permissions', true))
false

View file

@ -69,6 +69,7 @@ module.exports = class SpellView extends CocoView
@writable = false unless me.team in @spell.permissions.readwrite # TODO: make this do anything
@highlightCurrentLine = _.throttle @highlightCurrentLine, 100
$(window).on 'resize', @onWindowResize
@observing = @session.get('creator') isnt me.id
afterRender: ->
super()
@ -119,6 +120,7 @@ module.exports = class SpellView extends CocoView
name: 'run-code'
bindKey: {win: 'Shift-Enter|Ctrl-Enter', mac: 'Shift-Enter|Command-Enter|Ctrl-Enter'}
exec: -> Backbone.Mediator.publish 'tome:manual-cast', {}
unless @observing
addCommand
name: 'run-code-real-time'
bindKey: {win: 'Ctrl-Shift-Enter', mac: 'Command-Shift-Enter|Ctrl-Shift-Enter'}

View file

@ -134,6 +134,7 @@ module.exports = class TomeView extends CocoView
language: language
spectateView: @options.spectateView
spectateOpponentCodeLanguage: @options.spectateOpponentCodeLanguage
observing: @options.observing
levelID: @options.levelID
level: @options.level

View file

@ -15,7 +15,8 @@ module.exports = class LeaderboardTabView extends CocoView
template: template
className: 'leaderboard-tab-view'
events: {}
events:
'click tbody tr.viewable': 'onClickRow'
constructor: (options) ->
super options
@ -67,3 +68,8 @@ module.exports = class LeaderboardTabView extends CocoView
@hasShown = true
topScores = new TopScoresCollection @level, @scoreType, @timespan
@sessions = @supermodel.loadCollection(topScores, 'sessions', null, 0).model
onClickRow: (e) ->
sessionID = $(e.target).closest('tr').data 'session-id'
url = "/play/level/#{@level.get('slug')}?session=#{sessionID}&observing=true"
window.open url, '_blank'

View file

@ -15,7 +15,10 @@ class LevelSessionHandler extends Handler
formatEntity: (req, document) ->
documentObject = super(req, document)
if req.user?.isAdmin() or req.user?.id is document.creator or ('employer' in (req.user?.get('permissions') ? []))
if req.user?.isAdmin() or
req.user?.id is document.creator or
('employer' in (req.user?.get('permissions') ? [])) or
!document.submittedCode # TODO: only allow leaderboard access to non-top-5 solutions
return documentObject
else
return _.omit documentObject, @privateProperties
@ -47,8 +50,9 @@ class LevelSessionHandler extends Handler
@sendSuccess res, documents
hasAccessToDocument: (req, document, method=null) ->
return true if req.method is 'GET' and document.get('submitted')
return true if ('employer' in (req.user?.get('permissions') ? [])) and (method ? req.method).toLowerCase() is 'get'
get = (method ? req.method).toLowerCase() is 'get'
return true if get and document.get('submitted')
return true if get and ('employer' in (req.user?.get('permissions') ? []))
super(arguments...)
getCodeLanguageCounts: (req, res) ->