mirror of
https://github.com/codeninjasllc/codecombat.git
synced 2024-12-05 05:11:17 -05:00
395 lines
14 KiB
CoffeeScript
395 lines
14 KiB
CoffeeScript
CocoView = require 'views/core/CocoView'
|
|
CocoClass = require 'core/CocoClass'
|
|
Level = require 'models/Level'
|
|
LevelSession = require 'models/LevelSession'
|
|
CocoCollection = require 'collections/CocoCollection'
|
|
User = require 'models/User'
|
|
LeaderboardCollection = require 'collections/LeaderboardCollection'
|
|
{teamDataFromLevel} = require './utils'
|
|
ModelModal = require 'views/modal/ModelModal'
|
|
require 'vendor/d3'
|
|
|
|
HIGHEST_SCORE = 1000000
|
|
|
|
module.exports = class LadderTabView extends CocoView
|
|
id: 'ladder-tab-view'
|
|
template: require 'templates/play/ladder/ladder-tab-view'
|
|
|
|
events:
|
|
'click .connect-facebook': 'onConnectFacebook'
|
|
'click .connect-google-plus': 'onConnectGPlus'
|
|
'click .name-col-cell': 'onClickPlayerName'
|
|
'click .spectate-cell': 'onClickSpectateCell'
|
|
'click .load-more-ladder-entries': 'onLoadMoreLadderEntries'
|
|
|
|
# Refactored, to-reimplement
|
|
# subscriptions:
|
|
# 'auth:facebook-api-loaded': 'checkFriends'
|
|
# 'auth:gplus-api-loaded': 'checkFriends'
|
|
# 'auth:logged-in-with-facebook': 'onConnectedWithFacebook'
|
|
# 'auth:logged-in-with-gplus': 'onConnectedWithGPlus'
|
|
|
|
constructor: (options, @level, @sessions) ->
|
|
super(options)
|
|
@teams = teamDataFromLevel @level
|
|
@leaderboards = {}
|
|
@refreshLadder()
|
|
# Trying not loading the FP/G+ stuff for now to see if anyone complains they were using it so we can have just two columns.
|
|
#@socialNetworkRes = @supermodel.addSomethingResource('social_network_apis', 0)
|
|
#@checkFriends()
|
|
|
|
checkFriends: ->
|
|
return # Skipping for now
|
|
return if @checked or (not window.FB) or (not window.gapi)
|
|
@checked = true
|
|
|
|
# @addSomethingToLoad('facebook_status')
|
|
|
|
@fbStatusRes = @supermodel.addSomethingResource('facebook_status', 0)
|
|
@fbStatusRes.load()
|
|
|
|
FB.getLoginStatus (response) =>
|
|
return if @destroyed
|
|
@facebookStatus = response.status
|
|
@loadFacebookFriends() if @facebookStatus is 'connected'
|
|
@fbStatusRes.markLoaded()
|
|
|
|
if application.gplusHandler.loggedIn is undefined
|
|
@listenToOnce(application.gplusHandler, 'checked-state', @gplusSessionStateLoaded)
|
|
else
|
|
@gplusSessionStateLoaded()
|
|
|
|
@socialNetworkRes.markLoaded()
|
|
|
|
# FACEBOOK
|
|
|
|
onConnectFacebook: ->
|
|
@connecting = true
|
|
FB.login()
|
|
|
|
onConnectedWithFacebook: -> location.reload() if @connecting
|
|
|
|
loadFacebookFriends: ->
|
|
# @addSomethingToLoad('facebook_friends')
|
|
|
|
@fbFriendRes = @supermodel.addSomethingResource('facebook_friends', 0)
|
|
@fbFriendRes.load()
|
|
|
|
FB.api '/me/friends', @onFacebookFriendsLoaded
|
|
|
|
onFacebookFriendsLoaded: (response) =>
|
|
@facebookData = response.data
|
|
@loadFacebookFriendSessions()
|
|
@fbFriendRes.markLoaded()
|
|
|
|
loadFacebookFriendSessions: ->
|
|
levelFrag = "#{@level.get('original')}.#{@level.get('version').major}"
|
|
url = "/db/level/#{levelFrag}/leaderboard_facebook_friends"
|
|
|
|
@fbFriendSessionRes = @supermodel.addRequestResource('facebook_friend_sessions', {
|
|
url: url
|
|
data: { friendIDs: (f.id for f in @facebookData) }
|
|
method: 'POST'
|
|
success: @onFacebookFriendSessionsLoaded
|
|
})
|
|
@fbFriendSessionRes.load()
|
|
|
|
onFacebookFriendSessionsLoaded: (result) =>
|
|
friendsMap = {}
|
|
friendsMap[friend.id] = friend.name for friend in @facebookData
|
|
for friend in result
|
|
friend.name = friendsMap[friend.facebookID]
|
|
friend.otherTeam = if friend.team is 'humans' then 'ogres' else 'humans'
|
|
friend.imageSource = "http://graph.facebook.com/#{friend.facebookID}/picture"
|
|
@facebookFriendSessions = result
|
|
@render() # because the ladder tab renders before waiting for fb to finish
|
|
|
|
# GOOGLE PLUS
|
|
|
|
onConnectGPlus: ->
|
|
@connecting = true
|
|
@listenToOnce application.gplusHandler, 'logged-in', @onConnectedWithGPlus
|
|
application.gplusHandler.reauthorize()
|
|
|
|
onConnectedWithGPlus: -> location.reload() if @connecting
|
|
|
|
gplusSessionStateLoaded: ->
|
|
if application.gplusHandler.loggedIn
|
|
#@addSomethingToLoad('gplus_friends')
|
|
@gpFriendRes = @supermodel.addSomethingResource('gplus_friends', 0)
|
|
@gpFriendRes.load()
|
|
application.gplusHandler.loadFriends @gplusFriendsLoaded
|
|
|
|
gplusFriendsLoaded: (friends) =>
|
|
@gplusData = friends.items
|
|
@loadGPlusFriendSessions()
|
|
@gpFriendRes.markLoaded()
|
|
|
|
loadGPlusFriendSessions: ->
|
|
levelFrag = "#{@level.get('original')}.#{@level.get('version').major}"
|
|
url = "/db/level/#{levelFrag}/leaderboard_gplus_friends"
|
|
|
|
@gpFriendSessionRes = @supermodel.addRequestResource('gplus_friend_sessions', {
|
|
url: url
|
|
data: { friendIDs: (f.id for f in @gplusData) }
|
|
method: 'POST'
|
|
success: @onGPlusFriendSessionsLoaded
|
|
})
|
|
@gpFriendSessionRes.load()
|
|
|
|
onGPlusFriendSessionsLoaded: (result) =>
|
|
friendsMap = {}
|
|
friendsMap[friend.id] = friend for friend in @gplusData
|
|
for friend in result
|
|
friend.name = friendsMap[friend.gplusID].displayName
|
|
friend.otherTeam = if friend.team is 'humans' then 'ogres' else 'humans'
|
|
friend.imageSource = friendsMap[friend.gplusID].image.url
|
|
@gplusFriendSessions = result
|
|
@render() # because the ladder tab renders before waiting for gplus to finish
|
|
|
|
# LADDER LOADING
|
|
|
|
refreshLadder: ->
|
|
# Only do this so often if not in a league; servers cache a lot of this data for a few minutes anyway.
|
|
return if not @options.league and (new Date() - 2 * 60 * 1000 < @lastRefreshTime)
|
|
@lastRefreshTime = new Date()
|
|
@supermodel.resetProgress()
|
|
@ladderLimit ?= parseInt @getQueryVariable('top_players', if @options.league then 100 else 20)
|
|
for team in @teams
|
|
if oldLeaderboard = @leaderboards[team.id]
|
|
@supermodel.removeModelResource oldLeaderboard
|
|
oldLeaderboard.destroy()
|
|
teamSession = _.find @sessions.models, (session) -> session.get('team') is team.id
|
|
@leaderboards[team.id] = new LeaderboardData(@level, team.id, teamSession, @ladderLimit, @options.league)
|
|
@leaderboardRes = @supermodel.addModelResource(@leaderboards[team.id], 'leaderboard', {cache: false}, 3)
|
|
@leaderboardRes.load()
|
|
|
|
render: ->
|
|
super()
|
|
|
|
@$el.find('.histogram-display').each (i, el) =>
|
|
histogramWrapper = $(el)
|
|
team = _.find @teams, name: histogramWrapper.data('team-name')
|
|
histogramData = null
|
|
$.when(
|
|
level = "#{@level.get('original')}.#{@level.get('version').major}"
|
|
url = "/db/level/#{level}/histogram_data?team=#{team.name.toLowerCase()}"
|
|
url += '&leagues.leagueID=' + @options.league.id if @options.league
|
|
$.get url, (data) -> histogramData = data
|
|
).then =>
|
|
@generateHistogram(histogramWrapper, histogramData, team.name.toLowerCase()) unless @destroyed
|
|
|
|
getRenderData: ->
|
|
ctx = super()
|
|
ctx.level = @level
|
|
ctx.link = "/play/level/#{@level.get('name')}"
|
|
ctx.teams = @teams
|
|
team.leaderboard = @leaderboards[team.id] for team in @teams
|
|
ctx.levelID = @levelID
|
|
ctx.friends = @consolidateFriends()
|
|
ctx.onFacebook = @facebookStatus is 'connected'
|
|
ctx.onGPlus = application.gplusHandler.loggedIn
|
|
ctx.capitalize = _.string.capitalize
|
|
ctx.league = @options.league
|
|
ctx._ = _
|
|
ctx.moment = moment
|
|
ctx
|
|
|
|
generateHistogram: (histogramElement, histogramData, teamName) ->
|
|
#renders twice, hack fix
|
|
if $('#' + histogramElement.attr('id')).has('svg').length then return
|
|
histogramData = histogramData.map (d) -> d*100
|
|
|
|
margin =
|
|
top: 20
|
|
right: 20
|
|
bottom: 30
|
|
left: 15
|
|
|
|
width = 470 - margin.left - margin.right
|
|
height = 125 - margin.top - margin.bottom
|
|
|
|
formatCount = d3.format(',.0')
|
|
|
|
minX = Math.floor(Math.min(histogramData...) / 1000) * 1000
|
|
maxX = Math.ceil(Math.max(histogramData...) / 1000) * 1000
|
|
x = d3.scale.linear().domain([minX, maxX]).range([0, width])
|
|
data = d3.layout.histogram().bins(x.ticks(20))(histogramData)
|
|
y = d3.scale.linear().domain([0, d3.max(data, (d) -> d.y)]).range([height, 10])
|
|
|
|
#create the x axis
|
|
xAxis = d3.svg.axis().scale(x).orient('bottom').ticks(5).outerTickSize(0)
|
|
|
|
svg = d3.select('#' + histogramElement.attr('id')).append('svg')
|
|
.attr('width', width + margin.left + margin.right)
|
|
.attr('height', height + margin.top + margin.bottom)
|
|
.append('g')
|
|
.attr('transform', "translate(#{margin.left}, #{margin.top})")
|
|
barClass = 'bar'
|
|
if teamName.toLowerCase() is 'ogres' then barClass = 'ogres-bar'
|
|
if teamName.toLowerCase() is 'humans' then barClass = 'humans-bar'
|
|
|
|
bar = svg.selectAll('.bar')
|
|
.data(data)
|
|
.enter().append('g')
|
|
.attr('class', barClass)
|
|
.attr('transform', (d) -> "translate(#{x(d.x)}, #{y(d.y)})")
|
|
|
|
bar.append('rect')
|
|
.attr('x', 1)
|
|
.attr('width', width/20)
|
|
.attr('height', (d) -> height - y(d.y))
|
|
if session = @leaderboards[teamName].session
|
|
if @options.league
|
|
playerScore = (_.find(session.get('leagues'), {leagueID: @options.league.id})?.stats.totalScore or 10) * 100
|
|
else
|
|
playerScore = session.get('totalScore') * 100
|
|
scorebar = svg.selectAll('.specialbar')
|
|
.data([playerScore])
|
|
.enter().append('g')
|
|
.attr('class', 'specialbar')
|
|
.attr('transform', "translate(#{x(playerScore)}, 0)")
|
|
|
|
scorebar.append('rect')
|
|
.attr('x', 1)
|
|
.attr('width', 3)
|
|
.attr('height', height)
|
|
rankClass = 'rank-text'
|
|
if teamName.toLowerCase() is 'ogres' then rankClass = 'rank-text ogres-rank-text'
|
|
if teamName.toLowerCase() is 'humans' then rankClass = 'rank-text humans-rank-text'
|
|
|
|
message = "#{histogramData.length} players"
|
|
if @leaderboards[teamName].session?
|
|
if @options.league
|
|
# TODO: fix server handler to properly fetch myRank with a leagueID
|
|
message = "#{histogramData.length} players in league"
|
|
else if @leaderboards[teamName].myRank <= histogramData.length
|
|
message = "##{@leaderboards[teamName].myRank} of #{histogramData.length}"
|
|
else
|
|
message = 'Rank your session!'
|
|
svg.append('g')
|
|
.append('text')
|
|
.attr('class', rankClass)
|
|
.attr('y', 0)
|
|
.attr('text-anchor', 'end')
|
|
.attr('x', width)
|
|
.text(message)
|
|
|
|
#Translate the x-axis up
|
|
svg.append('g')
|
|
.attr('class', 'x axis')
|
|
.attr('transform', 'translate(0, ' + height + ')')
|
|
.call(xAxis)
|
|
|
|
consolidateFriends: ->
|
|
allFriendSessions = (@facebookFriendSessions or []).concat(@gplusFriendSessions or [])
|
|
sessions = _.uniq allFriendSessions, false, (session) -> session._id
|
|
if @options.league
|
|
sessions = _.sortBy sessions, (session) -> _.find(session.leagues, leagueID: @options.league.id)?.stats.totalScore ? (session.totalScore / 2)
|
|
else
|
|
sessions = _.sortBy sessions, 'totalScore'
|
|
sessions.reverse()
|
|
sessions
|
|
|
|
# Admin view of players' code
|
|
onClickPlayerName: (e) ->
|
|
return unless me.isAdmin()
|
|
row = $(e.target).parent()
|
|
player = new User _id: row.data 'player-id'
|
|
session = new LevelSession _id: row.data 'session-id'
|
|
@openModalView new ModelModal models: [session, player]
|
|
|
|
onClickSpectateCell: (e) ->
|
|
cell = $(e.target).closest '.spectate-cell'
|
|
row = cell.parent()
|
|
table = row.closest('table')
|
|
wasSelected = cell.hasClass 'selected'
|
|
table.find('.spectate-cell.selected').removeClass 'selected'
|
|
cell = $(e.target).closest('.spectate-cell').toggleClass 'selected', not wasSelected
|
|
sessionID = row.data 'session-id'
|
|
teamID = table.data 'team'
|
|
@spectateTargets ?= {}
|
|
@spectateTargets[teamID] = if wasSelected then null else sessionID
|
|
console.log @spectateTargets, cell, row, table
|
|
|
|
onLoadMoreLadderEntries: (e) ->
|
|
@ladderLimit ?= 100
|
|
@ladderLimit += 100
|
|
@lastRefreshTime = null
|
|
@refreshLadder()
|
|
|
|
module.exports.LeaderboardData = LeaderboardData = class LeaderboardData extends CocoClass
|
|
###
|
|
Consolidates what you need to load for a leaderboard into a single Backbone Model-like object.
|
|
###
|
|
|
|
constructor: (@level, @team, @session, @limit, @league) ->
|
|
super()
|
|
|
|
collectionParameters: (parameters) ->
|
|
parameters.team = @team
|
|
parameters['leagues.leagueID'] = @league.id if @league
|
|
parameters
|
|
|
|
fetch: ->
|
|
console.warn 'Already have top players on', @ if @topPlayers
|
|
|
|
@topPlayers = new LeaderboardCollection(@level, @collectionParameters(order: -1, scoreOffset: HIGHEST_SCORE, limit: @limit))
|
|
promises = []
|
|
promises.push @topPlayers.fetch cache: false
|
|
|
|
if @session
|
|
if @league
|
|
score = _.find(@session.get('leagues'), {leagueID: @league.id})?.stats.totalScore
|
|
else
|
|
score = @session.get('totalScore')
|
|
if score
|
|
@playersAbove = new LeaderboardCollection(@level, @collectionParameters(order: 1, scoreOffset: score, limit: 4))
|
|
promises.push @playersAbove.fetch cache: false
|
|
@playersBelow = new LeaderboardCollection(@level, @collectionParameters(order: -1, scoreOffset: score, limit: 4))
|
|
promises.push @playersBelow.fetch cache: false
|
|
level = "#{@level.get('original')}.#{@level.get('version').major}"
|
|
success = (@myRank) =>
|
|
loadURL = "/db/level/#{level}/leaderboard_rank?scoreOffset=#{score}&team=#{@team}"
|
|
loadURL += '&leagues.leagueID=' + @league.id if @league
|
|
promises.push $.ajax(loadURL, cache: false, success: success)
|
|
@promise = $.when(promises...)
|
|
@promise.then @onLoad
|
|
@promise.fail @onFail
|
|
@promise
|
|
|
|
onLoad: =>
|
|
return if @destroyed or not @topPlayers.loaded
|
|
@loaded = true
|
|
@loading = false
|
|
@trigger 'sync', @
|
|
# TODO: cache user ids -> names mapping, and load them here as needed,
|
|
# and apply them to sessions. Fetching each and every time is too costly.
|
|
|
|
onFail: (resource, jqxhr) =>
|
|
return if @destroyed
|
|
@trigger 'error', @, jqxhr
|
|
|
|
inTopSessions: ->
|
|
return me.id in (session.attributes.creator for session in @topPlayers.models)
|
|
|
|
nearbySessions: ->
|
|
if @league
|
|
score = _.find(@session?.get('leagues'), {leagueID: @league.id})?.stats.totalScore
|
|
else
|
|
score = @session?.get('totalScore')
|
|
return [] unless score
|
|
l = []
|
|
above = @playersAbove.models
|
|
l = l.concat(above)
|
|
l.reverse()
|
|
l.push @session
|
|
l = l.concat(@playersBelow.models)
|
|
if @myRank
|
|
startRank = @myRank - 4
|
|
session.rank = startRank + i for session, i in l
|
|
l
|
|
|
|
allResources: ->
|
|
resources = [@topPlayers, @playersAbove, @playersBelow]
|
|
return (r for r in resources when r)
|