mirror of
https://github.com/codeninjasllc/codecombat.git
synced 2024-12-17 19:12:33 -05:00
Adding easy observing of leaderboard matches (except top 5).
This commit is contained in:
parent
c5e47fda50
commit
c977ecc16f
15 changed files with 91 additions and 53 deletions
|
@ -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
|
||||
|
|
|
@ -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.
|
||||
|
||||
|
||||
|
|
|
@ -16,17 +16,21 @@
|
|||
th
|
||||
text-align: center
|
||||
|
||||
.rank-cell
|
||||
font-weight: bold
|
||||
tbody
|
||||
tr.viewable
|
||||
cursor: pointer
|
||||
|
||||
.name-col-cell
|
||||
max-width: 150px
|
||||
white-space: nowrap
|
||||
overflow: hidden
|
||||
text-overflow: ellipsis
|
||||
.rank-cell
|
||||
font-weight: bold
|
||||
|
||||
.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
|
||||
.name-col-cell
|
||||
max-width: 150px
|
||||
white-space: nowrap
|
||||
overflow: hidden
|
||||
text-overflow: ellipsis
|
||||
|
||||
.hero-portrait-cell, .code-language-cell
|
||||
background: transparent url(/images/common/code_languages/javascript_small.png) no-repeat center center
|
||||
background-size: 30px 30px
|
||||
height: 30px
|
||||
width: 32px
|
||||
|
|
|
@ -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,17 +28,19 @@ else
|
|||
|
||||
.buttons-area
|
||||
|
||||
button.btn.btn-inverse#game-menu-button(title="Show game menu")
|
||||
.hamburger
|
||||
span.icon-bar
|
||||
span.icon-bar
|
||||
span.icon-bar
|
||||
span.game-menu-text(data-i18n="play_level.game_menu") Game Menu
|
||||
if !observing
|
||||
button.btn.btn-inverse#game-menu-button(title="Show game menu")
|
||||
.hamburger
|
||||
span.icon-bar
|
||||
span.icon-bar
|
||||
span.icon-bar
|
||||
span.game-menu-text(data-i18n="play_level.game_menu") Game Menu
|
||||
|
||||
if spectateGame
|
||||
button.btn.btn-xs.btn-inverse.banner#next-game-button(title="Next Game", data-i18n="play_level.next-game") Next game!
|
||||
|
||||
button.btn.btn-xs.btn-primary.banner#level-done-button(data-i18n="play_level.done") Done
|
||||
if !observing
|
||||
button.btn.btn-xs.btn-primary.banner#level-done-button(data-i18n="play_level.done") Done
|
||||
|
||||
if me.get('anonymous')
|
||||
button.btn.btn-xs.btn-primary.banner#control-bar-sign-up-button(data-toggle='coco-modal', data-target='core/AuthModal', data-i18n="signup.sign_up")
|
||||
|
|
|
@ -1,9 +1,10 @@
|
|||
button.btn.btn-lg.btn-illustrated.cast-button(title=castVerbose)
|
||||
span(data-i18n="play_level.tome_run_button_ran") Ran
|
||||
|
||||
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
|
||||
|
||||
button.btn.btn-lg.btn-illustrated.done-button.secret
|
||||
span(data-i18n="play_level.done") Done
|
||||
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
|
||||
|
||||
button.btn.btn-lg.btn-illustrated.done-button.secret
|
||||
span(data-i18n="play_level.done") Done
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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$/, ''
|
||||
|
|
|
@ -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,8 +135,9 @@ module.exports = class PlayLevelView extends RootView
|
|||
@loadEndTime = new Date()
|
||||
loadDuration = @loadEndTime - @loadStartTime
|
||||
console.debug "Level unveiled after #{(loadDuration / 1000).toFixed(2)}s"
|
||||
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
|
||||
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
|
||||
|
||||
# CocoView overridden methods ###############################################
|
||||
|
||||
|
@ -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
|
||||
|
||||
|
|
|
@ -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: ->
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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,14 +120,15 @@ 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', {}
|
||||
addCommand
|
||||
name: 'run-code-real-time'
|
||||
bindKey: {win: 'Ctrl-Shift-Enter', mac: 'Command-Shift-Enter|Ctrl-Shift-Enter'}
|
||||
exec: =>
|
||||
if @options.level.get('replayable') and (timeUntilResubmit = @session.timeUntilResubmit()) > 0
|
||||
Backbone.Mediator.publish 'tome:manual-cast-denied', timeUntilResubmit: timeUntilResubmit
|
||||
else
|
||||
Backbone.Mediator.publish 'tome:manual-cast', {realTime: true}
|
||||
unless @observing
|
||||
addCommand
|
||||
name: 'run-code-real-time'
|
||||
bindKey: {win: 'Ctrl-Shift-Enter', mac: 'Command-Shift-Enter|Ctrl-Shift-Enter'}
|
||||
exec: =>
|
||||
if @options.level.get('replayable') and (timeUntilResubmit = @session.timeUntilResubmit()) > 0
|
||||
Backbone.Mediator.publish 'tome:manual-cast-denied', timeUntilResubmit: timeUntilResubmit
|
||||
else
|
||||
Backbone.Mediator.publish 'tome:manual-cast', {realTime: true}
|
||||
addCommand
|
||||
name: 'no-op'
|
||||
bindKey: {win: 'Ctrl-S', mac: 'Command-S|Ctrl-S'}
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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'
|
||||
|
|
|
@ -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) ->
|
||||
|
|
Loading…
Reference in a new issue