This commit is contained in:
George Saines 2014-05-22 09:55:34 -07:00
commit a2fbe5865a
35 changed files with 562 additions and 278 deletions

View file

@ -378,6 +378,7 @@ self.onWorldLoaded = function onWorldLoaded() {
}
var t3 = new Date();
console.log("And it was so: (" + (diff / self.world.totalFrames).toFixed(3) + "ms per frame,", self.world.totalFrames, "frames)\nSimulation :", diff + "ms \nSerialization:", (t2 - t1) + "ms\nDelivery :", (t3 - t2) + "ms");
self.world.goalManager.destroy();
self.world = null;
};
@ -408,6 +409,7 @@ self.onWorldLoadProgress = function onWorldLoadProgress(progress) {
self.abort = function abort() {
if(self.world) {
self.world.abort();
self.world.goalManager.destroy();
self.world = null;
}
self.postMessage({type: 'abort'});

View file

@ -1,3 +1,4 @@
Backbone.Mediator.setValidationEnabled false
app = require 'application'
channelSchemas =
@ -17,6 +18,10 @@ definitionSchemas =
'misc': require './schemas/definitions/misc'
init = ->
# Set up Backbone.Mediator schemas
setUpDefinitions()
setUpChannels()
Backbone.Mediator.setValidationEnabled document.location.href.search(/codecombat.com/) is -1
app.initialize()
Backbone.history.start({ pushState: true })
handleNormalUrls()
@ -25,10 +30,6 @@ init = ->
treemaExt.setup()
filepicker.setKey('AvlkNoldcTOU4PvKi2Xm7z')
# Set up Backbone.Mediator schemas
setUpDefinitions()
setUpChannels()
$ -> init()
handleNormalUrls = ->

View file

@ -9,7 +9,7 @@ module.exports = class CocoClass
@nicksUsed: {}
@remainingNicks: []
@nextNick: ->
return "CocoClass " + classCount unless @nicks.length
return (@name or "CocoClass") + " " + classCount unless @nicks.length
@remainingNicks = if @remainingNicks.length then @remainingNicks else @nicks.slice()
baseNick = @remainingNicks.splice(Math.floor(Math.random() * @remainingNicks.length), 1)[0]
i = 0
@ -37,7 +37,7 @@ module.exports = class CocoClass
destroy: ->
# teardown subscriptions, prevent new ones
@stopListening?()
@off()
@off?()
@unsubscribeAll()
@stopListeningToShortcuts()
@constructor.nicksUsed[@nick] = false
@ -65,6 +65,7 @@ module.exports = class CocoClass
Backbone.Mediator.subscribe(channel, func, @)
unsubscribeAll: ->
return unless Backbone?.Mediator?
for channel, func of @subscriptions
func = utils.normalizeFunc(func, @)
Backbone.Mediator.unsubscribe(channel, func, @)

View file

@ -47,7 +47,9 @@ module.exports = class God extends CocoClass
setLevel: (@level) ->
setLevelSessionIDs: (@levelSessionIDs) ->
setGoalManager: (goalManager) -> @angelsShare.goalManager = goalManager
setGoalManager: (goalManager) ->
@angelsShare.goalManager?.destroy() unless @angelsShare.goalManager is goalManager
@angelsShare.goalManager = goalManager
setWorldClassMap: (worldClassMap) -> @angelsShare.worldClassMap = worldClassMap
onTomeCast: (e) ->

View file

@ -63,12 +63,14 @@ module.exports = class LevelLoader extends CocoClass
url += "?team=#{@team}" if @team
session = new LevelSession().setURL url
@session = @supermodel.loadModel(session, 'level_session', {cache:false}).model
@sessionResource = @supermodel.loadModel(session, 'level_session', {cache:false})
@session = @sessionResource.model
@session.once 'sync', -> @url = -> '/db/level.session/' + @id
if @opponentSessionID
opponentSession = new LevelSession().setURL "/db/level_session/#{@opponentSessionID}"
@opponentSession = @supermodel.loadModel(opponentSession, 'opponent_session').model
@opponentSessionResource = @supermodel.loadModel(opponentSession, 'opponent_session')
@opponentSession = @opponentSessionResource.model
# Supermodel (Level) Loading
@ -111,6 +113,8 @@ module.exports = class LevelLoader extends CocoClass
@thangIDs = _.uniq thangIDs
@thangNames = new ThangNamesCollection(@thangIDs)
worldNecessities.push @supermodel.loadCollection(@thangNames, 'thang_names')
worldNecessities.push @sessionResource if @sessionResource?.isLoading
worldNecessities.push @opponentSessionResource if @opponentSessionResource?.isLoading
for obj in objUniq componentVersions
url = "/db/level.component/#{obj.original}/version/#{obj.majorVersion}"
@ -144,6 +148,7 @@ module.exports = class LevelLoader extends CocoClass
for thangTypeName in thangsToLoad
thangType = nameModelMap[thangTypeName]
continue if thangType.isFullyLoaded()
thangType.fetch()
thangType = @supermodel.loadModel(thangType, 'thang').model
res = @supermodel.addSomethingResource "sprite_sheet", 5
@ -165,16 +170,27 @@ module.exports = class LevelLoader extends CocoClass
app.tracker.updatePlayState(@level, @session) unless @headless
buildLoop: =>
return if @lastBuilt and new Date().getTime() - @lastBuilt < 10
return clearInterval @buildLoopInterval unless @spriteSheetsToBuild.length
someLeft = false
for spriteSheetResource, i in @spriteSheetsToBuild
if spriteSheetResource.thangType.loaded
@buildSpriteSheetsForThangType spriteSheetResource.thangType
@spriteSheetsToBuild.splice i, 1
@lastBuilt = new Date().getTime()
spriteSheetResource.markLoaded()
return
continue if spriteSheetResource.spriteSheetKeys
someLeft = true
thangType = spriteSheetResource.thangType
if thangType.loaded and not thangType.loading
keys = @buildSpriteSheetsForThangType spriteSheetResource.thangType
if keys and keys.length
@listenTo spriteSheetResource.thangType, 'build-complete', @onBuildComplete
spriteSheetResource.spriteSheetKeys = keys
else
spriteSheetResource.markLoaded()
clearInterval @buildLoopInterval unless someLeft
onBuildComplete: (e) ->
resource = null
for resource in @spriteSheetsToBuild
break if e.thangType is resource.thangType
resource.spriteSheetKeys = (k for k in resource.spriteSheetKeys when k isnt e.key)
resource.markLoaded() if resource.spriteSheetKeys.length is 0
denormalizeSession: ->
return if @headless or @sessionDenormalized or @spectateMode
@ -201,13 +217,16 @@ module.exports = class LevelLoader extends CocoClass
# queue = new createjs.LoadQueue()
# queue.loadFile('/file/'+f)
@grabThangTypeTeams() unless @thangTypeTeams
keys = []
for team in @thangTypeTeams[thangType.get('original')] ? [null]
spriteOptions = {resolutionFactor: SPRITE_RESOLUTION_FACTOR, async: false}
spriteOptions = {resolutionFactor: SPRITE_RESOLUTION_FACTOR, async: true}
if thangType.get('kind') is 'Floor'
spriteOptions.resolutionFactor = 2
if team and color = @teamConfigs[team]?.color
spriteOptions.colorConfig = team: color
@buildSpriteSheet thangType, spriteOptions
key = @buildSpriteSheet thangType, spriteOptions
if _.isString(key) then keys.push key
keys
grabThangTypeTeams: ->
@grabTeamConfigs()

View file

@ -35,6 +35,7 @@ module.exports = class DOMScriptModule extends ScriptModule
sides: dom.highlight.sides
offset: dom.highlight.offset
rotation: dom.highlight.rotation
note.event = _.pick note.event, (value) -> not _.isUndefined value
@maybeApplyDelayToNote note
note

View file

@ -52,12 +52,14 @@ module.exports = ScriptManager = class ScriptManager extends CocoClass
@debugScripts = @view.getQueryVariable 'dev'
@initProperties()
@addScriptSubscriptions()
@beginTicking()
setScripts: (@originalScripts) ->
@quiet = true
@initProperties()
@loadFromSession()
@quiet = false
@addScriptSubscriptions()
@run()
initProperties: ->
@ -74,6 +76,25 @@ module.exports = ScriptManager = class ScriptManager extends CocoClass
script.id = (idNum++).toString() unless script.id
callback = makeCallback(script.channel) # curry in the channel argument
@addNewSubscription(script.channel, callback)
beginTicking: ->
@tickInterval = setInterval @tick, 5000
tick: =>
scriptStates = {}
now = new Date()
for script in @scripts
scriptStates[script.id] =
timeSinceLastEnded: (if script.lastEnded then now - script.lastEnded else 0) / 1000
timeSinceLastTriggered: (if script.lastTriggered then now - script.lastTriggered else 0) / 1000
stateEvent =
scriptRunning: @currentNoteGroup?.scriptID or ''
noteGroupRunning: @currentNoteGroup?.name or ''
scriptStates: scriptStates
timeSinceLastScriptEnded: (if @lastScriptEnded then now - @lastScriptEnded else 0) / 1000
Backbone.Mediator.publish 'script-manager:tick', stateEvent
loadFromSession: ->
# load the queue with note groups to skip through
@ -88,6 +109,7 @@ module.exports = ScriptManager = class ScriptManager extends CocoClass
return unless script
@triggered.push(script.id)
noteChain = @processScript(script)
return unless noteChain
if scripts.currentScriptOffset
noteGroup.skipMe = true for noteGroup in noteChain[..scripts.currentScriptOffset-1]
@addNoteChain(noteChain, false)
@ -107,6 +129,7 @@ module.exports = ScriptManager = class ScriptManager extends CocoClass
@triggered.push(scriptID)
@ended.push(scriptID)
noteChain = @processScript(script)
return unless noteChain
noteGroup.skipMe = true for noteGroup in noteChain
@addNoteChain(noteChain, false)
@ -123,6 +146,7 @@ module.exports = ScriptManager = class ScriptManager extends CocoClass
destroy: ->
@onEndAll()
clearInterval @tickInterval
super()
# TRIGGERERING NOTES
@ -147,10 +171,11 @@ module.exports = ScriptManager = class ScriptManager extends CocoClass
continue unless @scriptPrereqsSatisfied(script)
continue unless scriptMatchesEventPrereqs(script, event)
# everything passed!
console.log "SCRIPT: Running script '#{script.id}'" if @debugScripts
console.debug "SCRIPT: Running script '#{script.id}'" if @debugScripts
script.lastTriggered = new Date().getTime()
@triggered.push(script.id) unless alreadyTriggered
noteChain = @processScript(script)
if not noteChain then return @trackScriptCompletions (script.id)
@addNoteChain(noteChain)
@run()
@ -159,10 +184,10 @@ module.exports = ScriptManager = class ScriptManager extends CocoClass
processScript: (script) ->
noteChain = script.noteChain
return null unless noteChain?.length
noteGroup.scriptID = script.id for noteGroup in noteChain
if noteChain.length
lastNoteGroup = noteChain[noteChain.length - 1]
lastNoteGroup.isLast = true
lastNoteGroup = noteChain[noteChain.length - 1]
lastNoteGroup.isLast = true
return noteChain
addNoteChain: (noteChain, clearYields=true) ->
@ -207,7 +232,7 @@ module.exports = ScriptManager = class ScriptManager extends CocoClass
@notifyScriptStateChanged()
@scriptInProgress = true
@currentTimeouts = []
console.log "SCRIPT: Starting note group '#{nextNoteGroup.name}'" if @debugScripts
console.debug "SCRIPT: Starting note group '#{nextNoteGroup.name}'" if @debugScripts
for module in nextNoteGroup.modules
@processNote(note, nextNoteGroup) for note in module.startNotes()
if nextNoteGroup.script.duration
@ -221,12 +246,12 @@ module.exports = ScriptManager = class ScriptManager extends CocoClass
@ignoreEvents = true
for noteGroup, i in @noteGroupQueue
break unless noteGroup.skipMe
console.log "SCRIPT: Skipping note group '#{noteGroup.name}'" if @debugScripts
console.debug "SCRIPT: Skipping note group '#{noteGroup.name}'" if @debugScripts
@processNoteGroup(noteGroup)
for module in noteGroup.modules
notes = module.skipNotes()
@processNote(note, noteGroup) for note in notes
@trackScriptCompletions(noteGroup)
@trackScriptCompletionsFromNoteGroup(noteGroup)
@noteGroupQueue = @noteGroupQueue[i..]
@ignoreEvents = false
@ -268,14 +293,13 @@ module.exports = ScriptManager = class ScriptManager extends CocoClass
return if @ending # kill infinite loops right here
@ending = true
return unless @currentNoteGroup?
console.log "SCRIPT: Ending note group '#{@currentNoteGroup.name}'" if @debugScripts
console.debug "SCRIPT: Ending note group '#{@currentNoteGroup.name}'" if @debugScripts
clearTimeout(timeout) for timeout in @currentTimeouts
for module in @currentNoteGroup.modules
@processNote(note, @currentNoteGroup) for note in module.endNotes()
Backbone.Mediator.publish 'note-group-ended' unless @quiet
@scriptInProgress = false
@ended.push(@currentNoteGroup.scriptID) if @currentNoteGroup.isLast
@trackScriptCompletions(@currentNoteGroup)
@trackScriptCompletionsFromNoteGroup(@currentNoteGroup)
@currentNoteGroup = null
unless @noteGroupQueue.length
@notifyScriptStateChanged()
@ -302,7 +326,7 @@ module.exports = ScriptManager = class ScriptManager extends CocoClass
for module in noteGroup.modules
notes = module.skipNotes()
@processNote(note, noteGroup) for note in notes unless @quiet
@trackScriptCompletions(noteGroup) unless @quiet
@trackScriptCompletionsFromNoteGroup(noteGroup) unless @quiet
@noteGroupQueue = []
@ -317,11 +341,18 @@ module.exports = ScriptManager = class ScriptManager extends CocoClass
Backbone.Mediator.publish 'level-enable-controls', {}
Backbone.Mediator.publish 'level-set-letterbox', { on: false }
trackScriptCompletions: (noteGroup) ->
return if @quiet
trackScriptCompletionsFromNoteGroup: (noteGroup) ->
return unless noteGroup.isLast
@ended.push(noteGroup.scriptID) unless noteGroup.scriptID in @ended
Backbone.Mediator.publish 'script:ended', {scriptID: noteGroup.scriptID}
@trackScriptCompletions(noteGroup.scriptID)
trackScriptCompletions: (scriptID) ->
return if @quiet
@ended.push(scriptID) unless scriptID in @ended
for script in @scripts
if script.id is scriptID
script.lastEnded = new Date()
@lastScriptEnded = new Date()
Backbone.Mediator.publish 'script:ended', {scriptID: scriptID}
notifyScriptStateChanged: ->
return if @quiet

View file

@ -36,4 +36,4 @@ module.exports = class ScriptModule extends CocoClass
Math.max(0, sums...)
maybeApplyDelayToNote: (note) ->
note.delay = @scrubbingTime + @movementTime
note.delay = (@scrubbingTime + @movementTime) or 0

View file

@ -23,7 +23,7 @@ module.exports = class Simulator extends CocoClass
@cleanupSimulation()
@god?.destroy()
super()
fetchAndSimulateOneGame: (humanGameID, ogresGameID) =>
return if @destroyed
$.ajax
@ -34,20 +34,22 @@ module.exports = class Simulator extends CocoClass
"humansGameID": humanGameID
"ogresGameID": ogresGameID
error: (errorData) ->
console.log "There was an error fetching two games! #{JSON.stringify errorData}"
console.warn "There was an error fetching two games! #{JSON.stringify errorData}"
success: (taskData) =>
return if @destroyed
@trigger 'statusUpdate', 'Setting up simulation...'
#refactor this
@task = new SimulationTask(taskData)
@supermodel ?= new SuperModel()
@supermodel.resetProgress()
@levelLoader = new LevelLoader supermodel: @supermodel, levelID: @task.getLevelName(), sessionID: @task.getFirstSessionID(), headless: true
if @supermodel.finished()
@simulateSingleGame()
else
@listenToOnce @supermodel, 'loaded-all', @simulateSingleGame
simulateSingleGame: ->
return if @destroyed
@trigger 'statusUpdate', 'Simulating...'
@ -55,32 +57,31 @@ module.exports = class Simulator extends CocoClass
@setupGod()
try
@commenceSingleSimulation()
catch err
console.log err
@handleSingleSimulationError()
catch error
@handleSingleSimulationError error
commenceSingleSimulation: ->
@god.createWorld @generateSpellsObject()
Backbone.Mediator.subscribeOnce 'god:infinite-loop', @handleSingleSimulationInfiniteLoop, @
Backbone.Mediator.subscribeOnce 'god:goals-calculated', @processSingleGameResults, @
handleSingleSimulationError: ->
console.log "There was an error simulating a single game!"
handleSingleSimulationError: (error) ->
console.error "There was an error simulating a single game!", error
if @options.headlessClient
console.log "GAMERESULT:tie"
process.exit(0)
@cleanupSimulation()
handleSingleSimulationInfiniteLoop: ->
console.log "There was an infinite loop in the single game!"
if @options.headlessClient
console.log "GAMERESULT:tie"
process.exit(0)
@cleanupSimulation()
processSingleGameResults: (simulationResults) ->
console.log "Processing results!"
taskResults = @formTaskResultsObject simulationResults
console.log "Processing results:", taskResults
humanSessionRank = taskResults.sessions[0].metrics.rank
ogreSessionRank = taskResults.sessions[1].metrics.rank
if @options.headlessClient
@ -93,12 +94,12 @@ module.exports = class Simulator extends CocoClass
process.exit(0)
else
@sendSingleGameBackToServer(taskResults)
@cleanupSimulation()
sendSingleGameBackToServer: (results) ->
@trigger 'statusUpdate', 'Simulation completed, sending results back to server!'
$.ajax
url: "/queue/scoring/recordTwoGames"
data: results
@ -107,8 +108,8 @@ module.exports = class Simulator extends CocoClass
success: @handleTaskResultsTransferSuccess
error: @handleTaskResultsTransferError
complete: @cleanupAndSimulateAnotherTask
fetchAndSimulateTask: =>
return if @destroyed
@ -134,7 +135,7 @@ module.exports = class Simulator extends CocoClass
console.error "There was a horrible Error: #{JSON.stringify errorData}"
@trigger 'statusUpdate', 'There was an error fetching games to simulate. Retrying in 10 seconds.'
@simulateAnotherTaskAfterDelay()
handleNoGamesResponse: ->
info = 'Finding game to simulate...'
@ -191,7 +192,7 @@ module.exports = class Simulator extends CocoClass
setupGod: ->
@god.setLevel @level.serialize @supermodel
@god.setLevelSessionIDs (session.id for session in @task.getSessions())
@god.setLevelSessionIDs (session.sessionID for session in @task.getSessions())
@god.setWorldClassMap @world.classMap
@god.setGoalManager new GoalManager(@world, @level.get 'goals')
@ -235,7 +236,7 @@ module.exports = class Simulator extends CocoClass
sendResultsBackToServer: (results) ->
@trigger 'statusUpdate', 'Simulation completed, sending results back to server!'
console.log "Sending result back to server!", results
console.log "Sending result back to server:", results
if @options.headlessClient and @options.testing
return @fetchAndSimulateTask()
@ -250,9 +251,10 @@ module.exports = class Simulator extends CocoClass
complete: @cleanupAndSimulateAnotherTask
handleTaskResultsTransferSuccess: (result) =>
return if @destroyed
console.log "Task registration result: #{JSON.stringify result}"
@trigger 'statusUpdate', 'Results were successfully sent back to server!'
console.log "Simulated by you: " + @simulatedByYou
console.log "Simulated by you:", @simulatedByYou
@simulatedByYou++
unless @options.headlessClient
simulatedBy = parseInt($('#simulated-by-you').text(), 10) + 1
@ -260,10 +262,12 @@ module.exports = class Simulator extends CocoClass
application.tracker?.trackEvent 'Simulator Result', label: "Success"
handleTaskResultsTransferError: (error) =>
return if @destroyed
@trigger 'statusUpdate', 'There was an error sending the results back to the server.'
console.log "Task registration error: #{JSON.stringify error}"
cleanupAndSimulateAnotherTask: =>
return if @destroyed
@cleanupSimulation()
@fetchAndSimulateTask()
@ -472,6 +476,5 @@ class SimulationTask
spellKey = pathComponents.join '/'
@thangSpells[thang.id].push spellKey
if not method.cloneOf and spellKey is desiredSpellKey
console.log "Setting #{desiredSpellKey} from world!"
#console.log "Setting #{desiredSpellKey} from world!"
return method.source

View file

@ -33,6 +33,7 @@ module.exports = CocoSprite = class CocoSprite extends CocoClass
camera: null
spriteSheetCache: null
showInvisible: false
async: true
possessed: false
flipped: false
@ -75,28 +76,32 @@ module.exports = CocoSprite = class CocoSprite extends CocoClass
@ranges = []
@handledDisplayEvents = {}
@age = 0
@stillLoading = true
if @thangType.isFullyLoaded()
@setupSprite()
else
@stillLoading = true
@thangType.fetch()
@listenToOnce(@thangType, 'sync', @setupSprite)
setupSprite: ->
for trigger, sounds of @thangType.get('soundTriggers') or {} when trigger isnt 'say'
AudioPlayer.preloadSoundReference sound for sound in sounds
@stillLoading = false
if @thangType.get('raster')
@stillLoading = false
@actions = {}
@isRaster = true
@setUpRasterImage()
else
@actions = @thangType.getActions()
@buildFromSpriteSheet @buildSpriteSheet()
@createMarks()
result = @buildSpriteSheet()
if _.isString result # async build
@listenToOnce @thangType, 'build-complete', @setupSprite
else
@stillLoading = false
@actions = @thangType.getActions()
@buildFromSpriteSheet result
@createMarks()
finishSetup: ->
return unless @thang
@updateBaseScale()
@scaleFactor = @thang.scaleFactor if @thang?.scaleFactor
@update true # Reflect initial scale and other state
@ -120,7 +125,7 @@ module.exports = CocoSprite = class CocoSprite extends CocoClass
buildSpriteSheet: ->
options = _.extend @options, @thang?.getSpriteOptions?() ? {}
options.colorConfig = @options.colorConfig if @options.colorConfig
options.async = false
options.async = @options.async
@thangType.getSpriteSheet options
setImageObject: (newImageObject) ->
@ -677,6 +682,7 @@ module.exports = CocoSprite = class CocoSprite extends CocoClass
updateGold: ->
# TODO: eventually this should be moved into some sort of team-based update
# rather than an each-thang-that-shows-gold-per-team thing.
return unless @thang
return if @thang.gold is @lastGold
gold = Math.floor @thang.gold
if @thang.world.age is 0
@ -752,7 +758,7 @@ module.exports = CocoSprite = class CocoSprite extends CocoClass
endFunc = =>
@lastTween = null
@imageObject.gotoAndPlay(endAnimation)
@imageObject.gotoAndPlay(endAnimation) unless @stillLoading
@shadow.action = 'idle'
@update true
@possessed = false

View file

@ -181,7 +181,8 @@ module.exports = class Mark extends CocoClass
return @listenToOnce(@thangType, 'sync', @onLoadedThangType) if not @thangType.loaded
CocoSprite = require './CocoSprite'
markSprite = new CocoSprite @thangType, @thangType.spriteOptions
# don't bother with making these render async for now, but maybe later for fun and more complexity of code
markSprite = new CocoSprite @thangType, {async: false}
markSprite.queueAction 'idle'
@mark = markSprite.imageObject
@markSprite = markSprite
@ -234,28 +235,27 @@ module.exports = class Mark extends CocoClass
if @name is 'debug' or (@name is 'shadow' and @sprite.thang?.shape in ["rectangle", "box"])
@mark.rotation = @sprite.thang.rotation * 180 / Math.PI
updateScale: ->
updateScale: (log) ->
if @name is 'bounds' and (@sprite.thang.width isnt @lastWidth or @sprite.thang.height isnt @lastHeight)
oldMark = @mark
@buildBounds()
oldMark.parent.addChild @mark
oldMark.parent.swapChildren oldMark, @mark
oldMark.parent.removeChild oldMark
if @markSprite?
@markSprite.scaleFactor = 1.2
@markSprite.updateScale()
return unless @name in ["selection", "target", "repair", "highlight"]
scale = 0.5
if @sprite?.imageObject
size = @sprite.getAverageDimension()
size += 60 if @name is 'selection'
size += 60 if @name is 'repair'
size *= @sprite.scaleFactor
scale = size / {selection: 128, target: 128, repair: 320, highlight: 160}[@name]
scale /= 3
if @sprite?.thang.spriteName.search(/(dungeon|indoor).wall/i) isnt -1
scale *= 2
if @markSprite?
@markSprite.scaleFactor = scale
@markSprite.updateScale()
else
@mark.scaleX = @mark.scaleY = Math.min 1, scale
if @name in ['selection', 'target', 'repair']
@mark.scaleY *= @camera.y2x # code applies perspective

View file

@ -21,21 +21,33 @@ module.exports = class MusicPlayer extends CocoClass
onPlayMusic: (e) ->
src = e.file
if src
src = "/file#{src}#{AudioPlayer.ext}"
return @currentMusic.play('none', 0, 0, -1, 0.3) if src is @currentMusic?.src
media = AudioPlayer.getStatus(src)
if not media?.loaded
AudioPlayer.preloadSound(src)
@standingBy = e
return
src = "/file#{e.file}#{AudioPlayer.ext}"
if (not e.file) or src is @currentMusic?.src
if e.play then @restartCurrentMusic() else @fadeOutCurrentMusic()
return
media = AudioPlayer.getStatus(src)
if not media?.loaded
AudioPlayer.preloadSound(src)
@standingBy = e
return
@standingBy = null
if @currentMusic
f = -> @stop()
createjs.Tween.get(@currentMusic).to({volume:0.0}, CROSSFADE_LENGTH).call(f)
@currentMusic = createjs.Sound.play(src, 'none', 0, 0, -1, 0.3) if src and e.play
@fadeOutCurrentMusic()
@startNewMusic(src) if e.play
restartCurrentMusic: ->
return unless @currentMusic
@currentMusic.play('none', 0, 0, -1, 0.3)
@updateMusicVolume()
fadeOutCurrentMusic: ->
return unless @currentMusic
f = -> @stop()
createjs.Tween.get(@currentMusic).to({volume:0.0}, CROSSFADE_LENGTH).call(f)
startNewMusic: (src) ->
@currentMusic = createjs.Sound.play(src, 'none', 0, 0, -1, 0.3) if src
return unless @currentMusic
@currentMusic.volume = 0.0
if me.get('music')

View file

@ -103,6 +103,7 @@ module.exports = Surface = class Surface extends CocoClass
@stage.removeAllChildren()
@stage.removeEventListener 'stagemousemove', @onMouseMove
@stage.removeEventListener 'stagemousedown', @onMouseDown
@stage.removeEventListener 'stagemouseup', @onMouseUp
@stage.removeAllEventListeners()
@stage.enableDOMEvents false
@stage.enableMouseOver 0
@ -281,6 +282,7 @@ module.exports = Surface = class Surface extends CocoClass
onSetPlaying: (e) ->
@playing = (e ? {}).playing ? true
@setPlayingCalled = true
if @playing and @currentFrame >= (@world.totalFrames - 5)
@currentFrame = 0
if @fastForwarding and not @playing
@ -352,6 +354,7 @@ module.exports = Surface = class Surface extends CocoClass
@casting = true
@wasPlayingWhenCastingBegan = @playing
Backbone.Mediator.publish 'level-set-playing', { playing: false }
@setPlayingCalled = false # don't overwrite playing settings if they changed by, say, scripts
if @coordinateDisplay?
@surfaceTextLayer.removeChild @coordinateDisplay
@ -370,7 +373,7 @@ module.exports = Surface = class Surface extends CocoClass
# This has a tendency to break scripts that are waiting for playback to change when the level is loaded
# so only run it after the first world is created.
Backbone.Mediator.publish 'level-set-playing', { playing: @wasPlayingWhenCastingBegan } unless event.firstWorld
Backbone.Mediator.publish 'level-set-playing', { playing: @wasPlayingWhenCastingBegan } unless event.firstWorld or @setPlayingCalled
fastForwardTo = null
if @playing
@ -414,6 +417,7 @@ module.exports = Surface = class Surface extends CocoClass
@stage.enableMouseOver(10)
@stage.addEventListener 'stagemousemove', @onMouseMove
@stage.addEventListener 'stagemousedown', @onMouseDown
@stage.addEventListener 'stagemouseup', @onMouseUp
@canvas.on 'mousewheel', @onMouseWheel
@hookUpChooseControls() if @options.choosing
createjs.Ticker.timingMode = createjs.Ticker.RAF_SYNCHED
@ -537,6 +541,11 @@ module.exports = Surface = class Surface extends CocoClass
onBackground = not @stage.hitTest e.stageX, e.stageY
Backbone.Mediator.publish 'surface:stage-mouse-down', onBackground: onBackground, x: e.stageX, y: e.stageY, originalEvent: e
onMouseUp: (e) =>
return if @disabled
onBackground = not @stage.hitTest e.stageX, e.stageY
Backbone.Mediator.publish 'surface:stage-mouse-up', onBackground: onBackground, x: e.stageX, y: e.stageY, originalEvent: e
onMouseWheel: (e) =>
# https://github.com/brandonaaron/jquery-mousewheel
e.preventDefault()

View file

@ -39,8 +39,6 @@ module.exports = class WizardSprite extends IndieSprite
else if options.name
@setNameLabel options.name
finishSetup: -> # No initial setup update needed.
makeIndieThang: (thangType, thangID, pos) ->
thang = super thangType, thangID, pos
thang.isSelectable = false
@ -49,6 +47,13 @@ module.exports = class WizardSprite extends IndieSprite
thang.pos.z += thang.bobHeight
thang
finishSetup: ->
@updateBaseScale()
@scaleFactor = @thang.scaleFactor if @thang?.scaleFactor
@updateScale()
@updateRotation()
# Don't call general update() because Thang isn't built yet
onPlayerStatesChanged: (e) ->
for playerID, state of e.states
continue unless playerID is @thang.id

View file

@ -14,6 +14,7 @@ module.exports = class GoalManager extends CocoClass
# If you want weird goals or hybrid goals, make a custom goal.
nextGoalID: 0
nicks: ["GoalManager"]
constructor: (@world, @initialGoals, @team) ->
super()

View file

@ -185,6 +185,22 @@ module.exports.thangNames = thangNames =
"Lacos"
"Upfish"
]
"Ogre Peon M": [
"Durbo"
"Kurger"
"Mudwich"
"Ba Bo"
"Zugger"
"Toe Pod"
]
"Ogre Peon F": [
"Iblet"
"Lorba"
"Zzoya"
"Yamra"
"Greeke"
"Vapa"
]
"Ogre M": [
"Krogg"
"Dronck"

View file

@ -3,10 +3,10 @@ module.exports = nativeDescription: "português do Brasil", englishDescription:
loading: "Carregando..."
saving: "Salvando..."
sending: "Enviando..."
# send: "Send"
send: "Enviar"
cancel: "Cancelar"
save: "Salvar"
# publish: "Publish"
publish: "Publicar"
create: "Criar"
delay_1_sec: "1 segundo"
delay_3_sec: "3 segundos"
@ -14,7 +14,7 @@ module.exports = nativeDescription: "português do Brasil", englishDescription:
manual: "Manual"
fork: "Fork"
play: "Jogar"
# retry: "Retry"
retry: "Tente novamente"
# watch: "Watch"
# unwatch: "Unwatch"
# submit_patch: "Submit Patch"
@ -36,11 +36,11 @@ module.exports = nativeDescription: "português do Brasil", englishDescription:
nav:
play: "Jogar"
# community: "Community"
community: "Comunidade"
editor: "Editor"
blog: "Blog"
forum: "Fórum"
# account: "Account"
account: "Conta"
admin: "Administrador"
home: "Início"
contribute: "Contribuir"
@ -78,7 +78,7 @@ module.exports = nativeDescription: "português do Brasil", englishDescription:
creating: "Criando a nova conta..."
sign_up: "Criar conta"
log_in: "Entre com a senha"
# social_signup: "Or, you can sign up through Facebook or G+:"
social_signup: "Ou, você pode fazer login pelo Facebook ou G+:"
home:
slogan: "Aprenda a programar em JavaScript enquanto se diverte com um jogo."
@ -133,15 +133,15 @@ module.exports = nativeDescription: "português do Brasil", englishDescription:
wizard_settings:
title: "Configurações do Feiticeiro"
customize_avatar: "Personalize o seu Avatar"
# active: "Active"
# color: "Color"
# group: "Group"
active: "Ativo"
color: "Cor"
group: "Grupo"
clothes: "Roupas"
trim: "Aparar"
cloud: "Nuvem"
# team: "Team"
team: "Time"
spell: "Feitiço"
boots: "Boots"
boots: "Botas"
hue: "Matiz"
saturation: "Saturação"
lightness: "Luminosidade"
@ -152,7 +152,7 @@ module.exports = nativeDescription: "português do Brasil", englishDescription:
autosave: "As alterações serão salvas automaticamente."
me_tab: "Eu"
picture_tab: "Foto"
# upload_picture: "Upload a picture"
upload_picture: "Enviar uma foto"
wizard_tab: "Feiticeiro"
password_tab: "Senha"
emails_tab: "Emails"
@ -167,8 +167,8 @@ module.exports = nativeDescription: "português do Brasil", englishDescription:
# email_notifications_summary: "Controls for personalized, automatic email notifications related to your CodeCombat activity."
# email_any_notes: "Any Notifications"
# email_any_notes_description: "Disable to stop all activity notification emails."
# email_recruit_notes: "Job Opportunities"
# email_recruit_notes_description: "If you play really well, we may contact you about getting you a (better) job."
email_recruit_notes: "Oportunidades de emprego"
email_recruit_notes_description: "Se você jogar muito bem, nós podemos lhe contactar para lhe oferecer um emprego (melhor)"
contributor_emails: "Emails para as Classes de Contribuidores"
contribute_prefix: "Estamos procurando pessoas para se juntar à nossa turma! Confira a nossa "
contribute_page: "página de contribuição"
@ -180,22 +180,22 @@ module.exports = nativeDescription: "português do Brasil", englishDescription:
# job_profile: "Job Profile"
# job_profile_approved: "Your job profile has been approved by CodeCombat. Employers will be able to see it until you either mark it inactive or it has not been changed for four weeks."
# job_profile_explanation: "Hi! Fill this out, and we will get in touch about finding you a software developer job."
# sample_profile: "See a sample profile"
# view_profile: "View Your Profile"
sample_profile: "Veja um perfil de exemplo"
view_profile: "Visualizar seu perfil"
account_profile:
edit_settings: "Editar as configurações"
profile_for_prefix: "Perfil de "
profile_for_suffix: ""
# approved: "Approved"
# not_approved: "Not Approved"
approved: "Aprovado"
not_approved: "Não Aprovado"
# looking_for: "Looking for:"
# last_updated: "Last updated:"
# contact: "Contact"
last_updated: "Última atualização:"
contact: "Contato"
# work_experience: "Work Experience"
# education: "Education"
education: "Formação"
# our_notes: "Our Notes"
# projects: "Projects"
projects: "Projetos"
# employers:
# want_to_hire_our_players: "Want to hire expert CodeCombat players?"
@ -203,8 +203,8 @@ module.exports = nativeDescription: "português do Brasil", englishDescription:
# candidates_count_prefix: "We currently have "
# candidates_count_many: "many"
# candidates_count_suffix: "highly skilled and vetted developers looking for work."
# candidate_name: "Name"
# candidate_location: "Location"
candidate_name: "Nome"
candidate_location: "Localização"
# candidate_looking_for: "Looking For"
# candidate_role: "Role"
# candidate_top_skills: "Top Skills"
@ -421,7 +421,7 @@ module.exports = nativeDescription: "português do Brasil", englishDescription:
results: "Resultados"
description: "Descrição"
or: "ou"
# subject: "Subject"
subject: "Assunto"
email: "Email"
password: "Senha"
message: "Mensagem"
@ -437,7 +437,7 @@ module.exports = nativeDescription: "português do Brasil", englishDescription:
easy: "Fácil"
medium: "Médio"
hard: "Difícil"
# player: "Player"
player: "Jogador"
about:
who_is_codecombat: "Quem é CodeCombat?"
@ -705,8 +705,8 @@ module.exports = nativeDescription: "português do Brasil", englishDescription:
# unknown: "Unknown error."
# resources:
# your_sessions: "Your Sessions"
# level: "Level"
your_sessions: "Suas sessões"
level: "vel"
# social_network_apis: "Social Network APIs"
# facebook_status: "Facebook Status"
# facebook_friends: "Facebook Friends"
@ -719,27 +719,27 @@ module.exports = nativeDescription: "português do Brasil", englishDescription:
# patches: "Patches"
# patched_model: "Source Document"
# model: "Model"
# system: "System"
# component: "Component"
# components: "Components"
system: "Sistema"
component: "Componente"
components: "Componentes"
# thang: "Thang"
# thangs: "Thangs"
# level_session: "Your Session"
level_session: "Sua sessão"
# opponent_session: "Opponent Session"
# article: "Article"
# user_names: "User Names"
article: "Artigos"
user_names: "Nomes de usuário"
# thang_names: "Thang Names"
# files: "Files"
files: "Arquivos"
# top_simulators: "Top Simulators"
# source_document: "Source Document"
# document: "Document"
# sprite_sheet: "Sprite Sheet"
document: "Documento"
sprite_sheet: "Planilha"
# delta:
# added: "Added"
# modified: "Modified"
# deleted: "Deleted"
added: "Adicionado"
modified: "Modificado"
deleted: "Removido"
# moved_index: "Moved Index"
# text_diff: "Text Diff"
# merge_conflict_with: "MERGE CONFLICT WITH"
# no_changes: "No Changes"
merge_conflict_with: "CONFLITO DE MERGE COM"
no_changes: "Sem mudanças"

View file

@ -37,7 +37,7 @@ class CocoModel extends Backbone.Model
@loading = false
@markToRevert()
@loadFromBackup()
getNormalizedURL: -> "#{@urlRoot}/#{@id}"
set: ->
@ -69,11 +69,17 @@ class CocoModel extends Backbone.Model
@set 'editPath', document.location.pathname
options ?= {}
success = options.success
options.success = (resp) =>
error = options.error
options.success = (model, res) =>
@trigger "save:success", @
success(@, resp) if success
success(@, res) if success
@markToRevert()
@clearBackup()
options.error = (model, res) =>
error(@, res) if error
errorMessage = "Error saving #{@get('name') ? @type()}"
console.error errorMessage, res.responseJSON
noty text: "#{errorMessage}: #{res.status} #{res.statusText}", layout: 'topCenter', type: 'error', killer: false, timeout: 10000
@trigger "save", @
return super attrs, options
@ -164,7 +170,7 @@ class CocoModel extends Backbone.Model
getDelta: ->
differ = deltasLib.makeJSONDiffer()
differ.diff @_revertAttributes, @attributes
getDeltaWith: (otherModel) ->
differ = deltasLib.makeJSONDiffer()
differ.diff @attributes, otherModel.attributes

View file

@ -17,6 +17,16 @@ module.exports = class SuperModel extends Backbone.Model
# necessarily have the same model or collection that was passed in, if it was fetched from
# the cache.
report: ->
# Useful for debugging why a SuperModel never finishes loading.
console.info "SuperModel report ------------------------"
console.info "#{_.values(@resources).length} resources."
unfinished = []
for resource in _.values(@resources) when resource
console.info '\t', resource.name, "loaded", resource.isLoaded
unfinished.push resource unless resource.isLoaded
unfinished
loadModel: (model, name, fetchOptions, value=1) ->
cachedModel = @getModelByURL(model.getURL())
if cachedModel
@ -118,6 +128,9 @@ module.exports = class SuperModel extends Backbone.Model
@storeResource(res, value)
return res
removeModelResource: (modelOrCollection) ->
@removeResource _.find(@resources, (resource) -> resource?.model is modelOrCollection)
addRequestResource: (name, jqxhrOptions, value=1) ->
@checkName(name)
res = new RequestResource(name, jqxhrOptions, value)
@ -143,11 +156,20 @@ module.exports = class SuperModel extends Backbone.Model
@denom += value
_.defer @updateProgress if @denom
removeResource: (resource) ->
return unless @resources[resource.rid]
@resources[resource.rid] = null
--@num if resource.isLoaded
--@denom
_.defer @updateProgress
onResourceLoaded: (r) ->
return unless @resources[r.rid]
@num += r.value
_.defer @updateProgress
onResourceFailed: (source) ->
return unless @resources[r.rid]
@trigger('failed', source)
updateProgress: =>
@ -160,13 +182,13 @@ module.exports = class SuperModel extends Backbone.Model
@progress = newProg
@trigger('update-progress', @progress)
@trigger('loaded-all') if @finished()
setMaxProgress: (@maxProgress) ->
resetProgress: -> @progress = 0
clearMaxProgress: ->
@maxProgress = 1
_.defer @updateProgress
getProgress: -> return @progress
getResource: (rid) ->

View file

@ -62,7 +62,7 @@ module.exports = class ThangType extends CocoModel
@options = @fillOptions options
key = @spriteSheetKey(@options)
if ss = @spriteSheets[key] then return ss
return if @building[key]
return key if @building[key]
@t0 = new Date().getTime()
@initBuild(options)
@addGeneralFrames() unless @options.portraitOnly
@ -151,25 +151,34 @@ module.exports = class ThangType extends CocoModel
buildQueue.push @builder
@builder.t0 = new Date().getTime()
@builder.buildAsync() unless buildQueue.length > 1
@builder.on 'complete', @onBuildSpriteSheetComplete, @, true, key
return true
@builder.on 'complete', @onBuildSpriteSheetComplete, @, true, [@builder, key, @options]
@builder = null
return key
spriteSheet = @builder.build()
console.debug "Built #{@get('name')}#{if @options.portraitOnly then ' portrait' else ''} in #{new Date().getTime() - @t0}ms."
@logBuild @t0, false, @options.portraitOnly
@spriteSheets[key] = spriteSheet
delete @building[key]
@builder = null
spriteSheet
onBuildSpriteSheetComplete: (e, key) ->
console.log "Built #{@get('name')}#{if @options.portraitOnly then ' portrait' else ''} async in #{new Date().getTime() - @builder.t0}ms." if @builder
onBuildSpriteSheetComplete: (e, data) ->
[builder, key, options] = data
@logBuild builder.t0, true, options.portraitOnly
buildQueue = buildQueue.slice(1)
buildQueue[0].t0 = new Date().getTime() if buildQueue[0]
buildQueue[0]?.buildAsync()
@spriteSheets[key] = e.target.spriteSheet
delete @building[key]
@trigger 'build-complete'
@builder = null
@trigger 'build-complete', {key:key, thangType:@}
@vectorParser = null
logBuild: (startTime, async, portrait) ->
kind = if async then 'Async' else 'Sync '
portrait = if portrait then '(Portrait)' else ''
name = _.string.rpad @get('name'), 20
time = _.string.lpad '' + new Date().getTime() - startTime, 6
console.debug "Built sheet: #{name} #{time}ms #{kind} #{portrait}"
spriteSheetKey: (options) ->
colorConfigs = []
for groupName, config of options.colorConfig or {}
@ -196,6 +205,7 @@ module.exports = class ThangType extends CocoModel
options = if _.isPlainObject spriteOptionsOrKey then spriteOptionsOrKey else {}
options.portraitOnly = true
spriteSheet = @buildSpriteSheet(options)
return if _.isString spriteSheet
return unless spriteSheet
canvas = $("<canvas width='#{size}' height='#{size}'></canvas>")
stage = new createjs.Stage(canvas[0])

View file

@ -223,19 +223,19 @@ _.extend LevelSessionSchema.properties,
sessionID:
title: 'Opponent Session ID'
description: 'The session ID of an opponent.'
type: ['object', 'string']
type: ['object', 'string','null']
userID:
title: 'Opponent User ID'
description: 'The user ID of an opponent'
type: ['object','string']
type: ['object','string','null']
name:
title: 'Opponent name'
description: 'The name of the opponent'
type: 'string'
type: ['string','null']
totalScore:
title: 'Opponent total score'
description: 'The totalScore of a user when the match was computed'
type: ['number','string']
type: ['number','string', 'null']
metrics:
type: 'object'
properties:

View file

@ -15,8 +15,21 @@ module.exports =
type: "object"
properties:
response:
type: "string"
type: "object"
properties:
status: { type: "string" }
authResponse:
type: "object"
properties:
accessToken: { type: "string" }
expiresIn: { type: "number" }
signedRequest: { type: "string" }
userID: { type: "string" }
required: ["response"]
"facebook-logged-out": {}
"linkedin-loaded": {}
"gapi-loaded":
{} # TODO schema

View file

@ -53,8 +53,9 @@ module.exports =
"level:team-set":
{} # TODO schema
"level:docs-hidden":
{} # TODO schema
"level:docs-shown": {}
"level:docs-hidden": {}
"level:victory-hidden":
{} # TODO schema
@ -74,9 +75,32 @@ module.exports =
"script:ended":
{} # TODO schema
"end-all-scripts": {}
"script:state-changed":
{} # TODO schema
'script-manager:tick':
type: 'object'
additionalProperties: false
properties:
scriptRunning: { type: 'string' }
noteGroupRunning: { type: 'string' }
timeSinceLastScriptEnded: { type: 'number' }
scriptStates:
type: 'object'
additionalProperties:
title: 'Script State'
type: 'object'
additionalProperties: false
properties:
timeSinceLastEnded:
type: 'number'
description: 'seconds since this script ended last'
timeSinceLastTriggered:
type: 'number'
description: 'seconds since this script was triggered last'
"play-sound":
{} # TODO schema
@ -114,5 +138,21 @@ module.exports =
"level-scrub-back":
{} # TODO schema
"level-show-victory":
type: 'object'
additionalProperties: false
properties:
showModal: { type: 'boolean' }
"level-highlight-dom":
type: 'object'
additionalProperties: false
properties:
selector: { type: 'string' }
delay: { type: 'number' }
sides: { type: 'array', items: { 'enum': ['left', 'right', 'top', 'bottom'] }}
offset: { type: 'object' }
rotation: { type: 'number' }
"goal-manager:new-goal-states":
{} # TODO schema

View file

@ -638,7 +638,7 @@ block content
.tab-pane.well#rules
h1(data-i18n="ladder.tournament_rules") Tournament Rules
h2 General
p You don't have to buy anything to participate in the tournament, and trying to pay us won't increase your odds of winning.
p You don't have to buy anything to participate in the tournament, and trying to pay us won't increase your odds of winning. Although we don't anticipate the rules changing, they are subject to change.
h2 Dates and Times
p The tournament starts on Tuesday, May 20 at 8:30AM and ends on Tuesday, June 10 at 5:00PM PDT. After the tournament finishes, we will check the games manually to prevent duplicate entries and cheating. We will email all the winners within two weeks of the end date.

View file

@ -39,11 +39,10 @@ module.exports = class ThangsTabView extends View
'level-thang-edited': 'onLevelThangEdited'
'level-thang-done-editing': 'onLevelThangDoneEditing'
'level:view-switched': 'onViewSwitched'
'sprite:mouse-down': 'onSpriteMouseDown'
'sprite:dragged': 'onSpriteDragged'
'sprite:mouse-up': 'onSpriteMouseUp'
'sprite:double-clicked': 'onSpriteDoubleClicked'
'surface:stage-mouse-down': 'onStageMouseDown'
'surface:stage-mouse-up': 'onStageMouseUp'
events:
'click #extant-thangs-filter button': 'onFilterExtantThangs'
@ -108,7 +107,7 @@ module.exports = class ThangsTabView extends View
afterRender: ->
super()
return unless @supermodel.finished()
$('.tab-content').click @selectAddThang
$('.tab-content').mousedown @selectAddThang
$('#thangs-list').bind 'mousewheel', @preventBodyScrollingInThangList
@$el.find('#extant-thangs-filter button:first').button('toggle')
$(window).resize @onWindowResize
@ -181,13 +180,13 @@ module.exports = class ThangsTabView extends View
onSpriteMouseDown: (e) ->
# Sprite clicks happen after stage clicks, but we need to know whether a sprite is being clicked.
clearTimeout @backgroundAddClickTimeout
if e.originalEvent.nativeEvent.button == 2
@onSpriteContextMenu e
# clearTimeout @backgroundAddClickTimeout
# if e.originalEvent.nativeEvent.button == 2
# @onSpriteContextMenu e
onStageMouseDown: (e) ->
onStageMouseUp: (e) ->
if @addThangSprite
# If we click on the background, we need to add @addThangSprite, but not if onSpriteMouseDown will fire.
# If we click on the background, we need to add @addThangSprite, but not if onSpriteMouseUp will fire.
@backgroundAddClickTimeout = _.defer => @onExtantThangSelected {}
$('#contextmenu').hide()
@ -202,6 +201,9 @@ module.exports = class ThangsTabView extends View
@calculateMovement(stageX / w, stageY / h, w / h)
onSpriteMouseUp: (e) ->
clearTimeout @backgroundAddClickTimeout
if e.originalEvent.nativeEvent.button == 2
@onSpriteContextMenu e
clearInterval(@movementInterval) if @movementInterval?
@movementInterval = null
@surface.camera.dragDisabled = false

View file

@ -72,6 +72,9 @@ module.exports = class LadderSubmissionView extends CocoView
transpileSession: ->
submittedCode = @session.get('code')
language = @session.get('codeLanguage') or 'javascript'
@session.set('submittedCodeLanguage', language)
@session.save() # TODO: maybe actually use a callback to make sure this works?
transpiledCode = {}
for thang, spells of submittedCode
transpiledCode[thang] = {}
@ -80,7 +83,7 @@ module.exports = class LadderSubmissionView extends CocoView
#DRY this
aetherOptions =
problems: {}
language: "javascript"
language: language
functionName: spellID
functionParameters: []
yieldConditionally: spellID is "plan"

View file

@ -150,9 +150,12 @@ module.exports = class LadderTabView extends CocoView
# LADDER LOADING
refreshLadder: ->
@supermodel.resetProgress()
@ladderLimit ?= parseInt @getQueryVariable('top_players', 20)
for team in @teams
@leaderboards[team.id]?.destroy()
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)
@leaderboardRes = @supermodel.addModelResource(@leaderboards[team.id], 'leaderboard', 3)
@ -242,7 +245,7 @@ module.exports = class LadderTabView extends CocoView
if teamName.toLowerCase() is "humans" then rankClass = "rank-text humans-rank-text"
message = "#{histogramData.length} players"
if @leaderboards[teamName].session?
if @leaderboards[teamName].session?
if @leaderboards[teamName].myRank <= histogramData.length
message="##{@leaderboards[teamName].myRank} of #{histogramData.length}"
else

View file

@ -65,7 +65,7 @@ module.exports = class LadderView extends RootView
@insertSubView(@simulateTab = new SimulateTabView())
@refreshInterval = setInterval(@fetchSessionsAndRefreshViews.bind(@), 20 * 1000)
hash = document.location.hash[1..] if document.location.hash
if hash and not (hash in ['my-matches', 'simulate', 'ladder'])
if hash and not (hash in ['my-matches', 'simulate', 'ladder', 'prizes', 'rules'])
@showPlayModal(hash) if @sessions.loaded
fetchSessionsAndRefreshViews: ->

View file

@ -26,6 +26,9 @@ module.exports = class MyMatchesTabView extends CocoView
for session in @sessions.models
for match in (session.get('matches') or [])
id = match.opponents[0].userID
unless id
console.error "Found bad opponent ID in malformed match:", match, "from session", session
continue
ids.push id unless @nameMap[id]
return @finishRendering() unless ids.length
@ -35,7 +38,7 @@ module.exports = class MyMatchesTabView extends CocoView
for session in @sessions.models
for match in session.get('matches') or []
opponent = match.opponents[0]
@nameMap[opponent.userID] ?= nameMap[opponent.userID].name
@nameMap[opponent.userID] ?= nameMap[opponent.userID]?.name ? "<bad match data>"
@finishRendering()
$.ajax('/db/user/-/names', {
@ -94,6 +97,7 @@ module.exports = class MyMatchesTabView extends CocoView
afterRender: ->
super()
@removeSubView subview for key, subview of @subviews when subview instanceof LadderSubmissionView
@$el.find('.ladder-submission-view').each (i, el) =>
placeholder = $(el)
sessionID = placeholder.data('session-id')

View file

@ -13,6 +13,7 @@ module.exports = class DocsModal extends View
'enter': 'hide'
constructor: (options) ->
@firstOnly = options.firstOnly
@docs = options?.docs
general = @docs.generalArticles or []
specific = @docs.specificArticles or []
@ -25,6 +26,7 @@ module.exports = class DocsModal extends View
@docs = specific.concat(general)
@docs = $.extend(true, [], @docs)
@docs = [@docs[0]] if @firstOnly and @docs[0]
doc.html = marked(utils.i18n doc, 'body') for doc in @docs
doc.name = (utils.i18n doc, 'name') for doc in @docs
doc.slug = _.string.slugify(doc.name) for doc in @docs
@ -47,6 +49,10 @@ module.exports = class DocsModal extends View
clickTab: (e) =>
@$el.find('li.active').removeClass('active')
afterInsert: ->
super()
Backbone.Mediator.publish 'level:docs-shown'
onHidden: ->
Backbone.Mediator.publish 'level:docs-hidden'

View file

@ -21,8 +21,11 @@ module.exports = class ThangAvatarView extends View
unless @thangType.isFullyLoaded() or @thangType.loading
@thangType.fetch()
@supermodel.loadModel @thangType, 'thang'
# couldn't get the level view to load properly through the supermodel
# so just doing it manually this time.
@listenTo @thangType, 'sync', @render
@listenTo @thangType, 'build-complete', @render
getSpriteThangType: ->
thangs = @supermodel.getModels(ThangType)
@ -34,7 +37,7 @@ module.exports = class ThangAvatarView extends View
context = super context
context.thang = @thang
options = @thang?.getSpriteOptions() or {}
options.async = false
options.async = true
context.avatarURL = @thangType.getPortraitSource(options) unless @thangType.loading
context.includeName = @includeName
context

View file

@ -153,6 +153,11 @@ module.exports = class SpellView extends View
name: 'spell-beautify'
bindKey: {win: 'Ctrl-Shift-B', mac: 'Command-Shift-B|Ctrl-Shift-B'}
exec: -> Backbone.Mediator.publish 'spell-beautify'
addCommand
name: 'prevent-line-jump'
bindKey: {win: 'Ctrl-L', mac: 'Command-L'}
passEvent: true
exec: -> # just prevent default ACE go-to-line alert
fillACE: ->
@ace.setValue @spell.source

View file

@ -133,7 +133,10 @@ module.exports = class PlayLevelView extends View
showGuide: ->
@seenDocs = true
DocsModal = require './level/modal/docs_modal'
options = {docs: @levelLoader.level.get('documentation'), supermodel: @supermodel}
options =
docs: @levelLoader.level.get('documentation')
supermodel: @supermodel
firstOnly: true
@openModalView(new DocsModal(options), true)
Backbone.Mediator.subscribeOnce 'modal-closed', @onLevelStarted, @
return true
@ -287,6 +290,7 @@ module.exports = class PlayLevelView extends View
unless @isEditorPreview
@loadEndTime = new Date()
loadDuration = @loadEndTime - @loadStartTime
console.debug "Level unveiled after #{(loadDuration / 1000).toFixed(2)}s"
application.tracker?.trackEvent 'Finished Level Load', level: @levelID, label: @levelID, loadDuration: loadDuration
application.tracker?.trackTiming loadDuration, 'Level Load Time', @levelID, @levelID

View file

@ -55,6 +55,7 @@ addPairwiseTaskToQueue = (taskPair, cb) ->
if taskPairError? then return cb taskPairError
cb null
# We should rip these out, probably
module.exports.resimulateAllSessions = (req, res) ->
unless isUserAdmin req then return errors.unauthorized res, "Unauthorized. Even if you are authorized, you shouldn't do this"
@ -99,6 +100,30 @@ resimulateSession = (originalLevelID, levelMajorVersion, session, cb) =>
if taskPairError? then return cb taskPairError, null
cb null
selectRandomSkipIndex = (numberOfSessions) ->
numbers = [0...numberOfSessions]
numberWeights = []
lambda = 0.025
for number, index in numbers
numberWeights[index] = lambda*Math.exp(-1*lambda*number) + lambda/(numberOfSessions/15)
sum = numberWeights.reduce (a, b) -> a + b
for number,index in numberWeights
numberWeights[index] /= sum
rand = (min, max) -> Math.random() * (max - min) + min
totalWeight = 1
randomNumber = Math.random()
weightSum = 0
for number, i in numbers
weightSum += numberWeights[i]
if (randomNumber <= weightSum)
return numbers[i]
module.exports.getTwoGames = (req, res) ->
#if userIsAnonymous req then return errors.unauthorized(res, "You need to be logged in to get games.")
humansGameID = req.body.humansGameID
@ -106,53 +131,64 @@ module.exports.getTwoGames = (req, res) ->
unless ogresGameID and humansGameID
#fetch random games here
queryParams =
queryParams =
"levelID":"greed"
"submitted":true
"team":"humans"
selection = "team totalScore transpiledCode teamSpells levelID creatorName creator"
selection = "team totalScore transpiledCode teamSpells levelID creatorName creator submitDate"
LevelSession.count queryParams, (err, numberOfHumans) =>
query = LevelSession
.find(queryParams)
.limit(1)
.select(selection)
.skip(Math.floor(Math.random()*numberOfHumans))
.lean()
query.exec (err, randomSession) =>
if err? then return errors.serverError(res, "Couldn't select a top 15 random session!")
randomSession = randomSession[0]
queryParams =
"levelID":"greed"
"submitted":true
"totalScore":
$lte: randomSession.totalScore
"team": "ogres"
if err? then return errors.serverError(res, "Couldn't get the number of human games")
humanSkipCount = selectRandomSkipIndex(numberOfHumans)
ogreCountParams =
"levelID": "greed"
"submitted":true
"team":"ogres"
LevelSession.count ogreCountParams, (err, numberOfOgres) =>
if err? then return errors.serverError(res, "Couldnt' get the number of ogre games")
ogresSkipCount = selectRandomSkipIndex(numberOfOgres)
query = LevelSession
.find(queryParams)
.select(selection)
.sort(totalScore: -1)
.aggregate()
.match(queryParams)
.project(selection)
.sort({"submitDate": -1})
.skip(humanSkipCount)
.limit(1)
.lean()
query.exec (err, otherSession) =>
if err? then return errors.serverError(res, "Couldnt' select the other top 15 random session!")
otherSession = otherSession[0]
taskObject =
"messageGenerated": Date.now()
"sessions": []
for session in [randomSession, otherSession]
sessionInformation =
"sessionID": session._id
"team": session.team ? "No team"
"transpiledCode": session.transpiledCode
"teamSpells": session.teamSpells ? {}
"levelID": session.levelID
"creatorName": session.creatorName
"creator": session.creator
"totalScore": session.totalScore
taskObject.sessions.push sessionInformation
sendResponseObject req, res, taskObject
query.exec (err, randomSession) =>
if err? then return errors.serverError(res, "Couldn't select a random session! #{err}")
randomSession = randomSession[0]
queryParams =
"levelID":"greed"
"submitted":true
"team": "ogres"
query = LevelSession
.aggregate()
.match(queryParams)
.project(selection)
.sort({"submitDate": -1})
.skip(ogresSkipCount)
.limit(1)
query.exec (err, otherSession) =>
if err? then return errors.serverError(res, "Couldnt' select the other random session!")
otherSession = otherSession[0]
taskObject =
"messageGenerated": Date.now()
"sessions": []
for session in [randomSession, otherSession]
sessionInformation =
"sessionID": session._id
"team": session.team ? "No team"
"transpiledCode": session.transpiledCode
"teamSpells": session.teamSpells ? {}
"levelID": session.levelID
"creatorName": session.creatorName
"creator": session.creator
"totalScore": session.totalScore
taskObject.sessions.push sessionInformation
console.log "Dispatching random game between", taskObject.sessions[0].creatorName, "and", taskObject.sessions[1].creatorName
sendResponseObject req, res, taskObject
else
console.log "Directly simulating #{humansGameID} vs. #{ogresGameID}."
LevelSession.findOne(_id: humansGameID).lean().exec (err, humanSession) =>
if err? then return errors.serverError(res, "Couldn't find the human game")
LevelSession.findOne(_id: ogresGameID).lean().exec (err, ogreSession) =>
@ -167,23 +203,25 @@ module.exports.getTwoGames = (req, res) ->
"transpiledCode": session.transpiledCode
"teamSpells": session.teamSpells ? {}
"levelID": session.levelID
taskObject.sessions.push sessionInformation
sendResponseObject req, res, taskObject
module.exports.recordTwoGames = (req, res) ->
@clientResponseObject = req.body
sessions = req.body.sessions
console.log "Recording non-chained result of", sessions?[0]?.name, sessions[0]?.metrics?.rank, "and", sessions?[1]?.name, sessions?[1]?.metrics?.rank
yetiGuru = clientResponseObject: req.body, isRandomMatch: true
async.waterfall [
fetchLevelSession.bind(@)
updateSessions.bind(@)
indexNewScoreArray.bind(@)
addMatchToSessions.bind(@)
updateUserSimulationCounts.bind(@, req.user._id)
fetchLevelSession.bind(yetiGuru)
updateSessions.bind(yetiGuru)
indexNewScoreArray.bind(yetiGuru)
addMatchToSessions.bind(yetiGuru)
updateUserSimulationCounts.bind(yetiGuru, req.user._id)
], (err, successMessageObject) ->
if err? then return errors.serverError res, "There was an error recording the single game:#{err}"
sendResponseObject req, res, {"message":"The single game was submitted successfully!"}
module.exports.createNewTask = (req, res) ->
@ -193,12 +231,13 @@ module.exports.createNewTask = (req, res) ->
transpiledCode = req.body.transpiledCode
requestLevelMajorVersion = parseInt(req.body.levelMajorVersion)
yetiGuru = {}
async.waterfall [
validatePermissions.bind(@,req,requestSessionID)
fetchAndVerifyLevelType.bind(@,currentLevelID)
fetchSessionObjectToSubmit.bind(@, requestSessionID)
updateSessionToSubmit.bind(@, transpiledCode)
fetchInitialSessionsToRankAgainst.bind(@, requestLevelMajorVersion, originalLevelID)
validatePermissions.bind(yetiGuru,req,requestSessionID)
fetchAndVerifyLevelType.bind(yetiGuru,currentLevelID)
fetchSessionObjectToSubmit.bind(yetiGuru, requestSessionID)
updateSessionToSubmit.bind(yetiGuru, transpiledCode)
fetchInitialSessionsToRankAgainst.bind(yetiGuru, requestLevelMajorVersion, originalLevelID)
generateAndSendTaskPairsToTheQueue
], (err, successMessageObject) ->
if err? then return errors.serverError res, "There was an error submitting the game to the queue:#{err}"
@ -256,9 +295,9 @@ updateSessionToSubmit = (transpiledCode, sessionToUpdate, callback) ->
submittedCode: sessionToUpdate.code
transpiledCode: transpiledCode
submitDate: new Date()
meanStrength: 25
#meanStrength: 25 # Let's try not resetting the score on resubmission
standardDeviation: 25/3
totalScore: 10
#totalScore: 10 # Let's try not resetting the score on resubmission
numberOfWinsAndTies: 0
numberOfLosses: 0
isRanking: true
@ -300,13 +339,14 @@ generateAndSendTaskPairsToTheQueue = (sessionToRankAgainst,submittedSession, cal
module.exports.dispatchTaskToConsumer = (req, res) ->
yetiGuru = {}
async.waterfall [
checkSimulationPermissions.bind(@,req)
checkSimulationPermissions.bind(yetiGuru,req)
receiveMessageFromSimulationQueue
changeMessageVisibilityTimeout
parseTaskQueueMessage
constructTaskObject
constructTaskLogObject.bind(@, getUserIDFromRequest(req))
constructTaskLogObject.bind(yetiGuru, getUserIDFromRequest(req))
processTaskObject
], (err, taskObjectToSend) ->
if err?
@ -357,7 +397,6 @@ constructTaskObject = (taskMessageBody, message, callback) ->
"sessionID": session._id
"submitDate": session.submitDate
"team": session.team ? "No team"
"code": session.submittedCode
"transpiledCode": session.transpiledCode
"teamSpells": session.teamSpells ? {}
"levelID": session.levelID
@ -397,34 +436,38 @@ getSessionInformation = (sessionIDString, callback) ->
module.exports.processTaskResult = (req, res) ->
originalSessionID = req.body?.originalSessionID
async.waterfall [
verifyClientResponse.bind(@,req.body)
fetchTaskLog.bind(@)
checkTaskLog.bind(@)
deleteQueueMessage.bind(@)
fetchLevelSession.bind(@)
checkSubmissionDate.bind(@)
logTaskComputation.bind(@)
updateSessions.bind(@)
indexNewScoreArray.bind(@)
addMatchToSessions.bind(@)
updateUserSimulationCounts.bind(@, req.user._id)
determineIfSessionShouldContinueAndUpdateLog.bind(@)
findNearestBetterSessionID.bind(@)
addNewSessionsToQueue.bind(@)
], (err, results) ->
if err is "shouldn't continue"
markSessionAsDoneRanking originalSessionID, (err) ->
if err? then return sendResponseObject req, res, {"error":"There was an error marking the session as done ranking"}
sendResponseObject req, res, {"message":"The scores were updated successfully, person lost so no more games are being inserted!"}
else if err is "no session was found"
markSessionAsDoneRanking originalSessionID, (err) ->
if err? then return sendResponseObject req, res, {"error":"There was an error marking the session as done ranking"}
sendResponseObject req, res, {"message":"There were no more games to rank (game is at top)!"}
else if err?
errors.serverError res, "There was an error:#{err}"
else
sendResponseObject req, res, {"message":"The scores were updated successfully and more games were sent to the queue!"}
yetiGuru = {}
try
async.waterfall [
verifyClientResponse.bind(yetiGuru,req.body)
fetchTaskLog.bind(yetiGuru)
checkTaskLog.bind(yetiGuru)
deleteQueueMessage.bind(yetiGuru)
fetchLevelSession.bind(yetiGuru)
checkSubmissionDate.bind(yetiGuru)
logTaskComputation.bind(yetiGuru)
updateSessions.bind(yetiGuru)
indexNewScoreArray.bind(yetiGuru)
addMatchToSessions.bind(yetiGuru)
updateUserSimulationCounts.bind(yetiGuru, req.user._id)
determineIfSessionShouldContinueAndUpdateLog.bind(yetiGuru)
findNearestBetterSessionID.bind(yetiGuru)
addNewSessionsToQueue.bind(yetiGuru)
], (err, results) ->
if err is "shouldn't continue"
markSessionAsDoneRanking originalSessionID, (err) ->
if err? then return sendResponseObject req, res, {"error":"There was an error marking the session as done ranking"}
sendResponseObject req, res, {"message":"The scores were updated successfully, person lost so no more games are being inserted!"}
else if err is "no session was found"
markSessionAsDoneRanking originalSessionID, (err) ->
if err? then return sendResponseObject req, res, {"error":"There was an error marking the session as done ranking"}
sendResponseObject req, res, {"message":"There were no more games to rank (game is at top)!"}
else if err?
errors.serverError res, "There was an error:#{err}"
else
sendResponseObject req, res, {"message":"The scores were updated successfully and more games were sent to the queue!"}
catch e
errors.serverError res, "There was an error processing the task result!"
verifyClientResponse = (responseObject, callback) ->
#TODO: better verification
@ -432,6 +475,7 @@ verifyClientResponse = (responseObject, callback) ->
callback "The response to that query is required to be a JSON object."
else
@clientResponseObject = responseObject
#log.info "Verified client response!"
callback null, responseObject
@ -459,6 +503,7 @@ deleteQueueMessage = (callback) ->
fetchLevelSession = (callback) ->
findParameters =
_id: @clientResponseObject.originalSessionID
query = LevelSession
.findOne(findParameters)
.lean()
@ -490,11 +535,12 @@ updateSessions = (callback) ->
async.map sessionIDs, retrieveOldSessionData, (err, oldScores) =>
if err? then callback err, {"error": "There was an error retrieving the old scores"}
oldScoreArray = _.toArray putRankingFromMetricsIntoScoreObject @clientResponseObject, oldScores
newScoreArray = bayes.updatePlayerSkills oldScoreArray
saveNewScoresToDatabase newScoreArray, callback
try
oldScoreArray = _.toArray putRankingFromMetricsIntoScoreObject @clientResponseObject, oldScores
newScoreArray = bayes.updatePlayerSkills oldScoreArray
saveNewScoresToDatabase newScoreArray, callback
catch e
callback e
saveNewScoresToDatabase = (newScoreArray, callback) ->
async.eachSeries newScoreArray, updateScoreInSession, (err) ->
@ -541,7 +587,8 @@ addMatchToSessions = (newScoreObject, callback) ->
#log.info "Writing match object to database..."
#use bind with async to do the writes
sessionIDs = _.pluck @clientResponseObject.sessions, 'sessionID'
async.each sessionIDs, updateMatchesInSession.bind(@,matchObject), (err) -> callback err
async.each sessionIDs, updateMatchesInSession.bind(@,matchObject), (err) ->
callback err
updateMatchesInSession = (matchObject, sessionID, callback) ->
currentMatchObject = {}
@ -562,7 +609,11 @@ updateMatchesInSession = (matchObject, sessionID, callback) ->
updateUserSimulationCounts = (reqUserID,callback) ->
incrementUserSimulationCount reqUserID, 'simulatedBy', (err) =>
if err? then return callback err
incrementUserSimulationCount @levelSession.creator, 'simulatedFor', callback
console.log "Incremented user simulation count!"
unless @isRandomMatch
incrementUserSimulationCount @levelSession.creator, 'simulatedFor', callback
else
callback null
incrementUserSimulationCount = (userID, type, callback) =>
inc = {}
@ -605,13 +656,16 @@ determineIfSessionShouldContinueAndUpdateLog = (cb) ->
findNearestBetterSessionID = (cb) ->
levelOriginalID = @levelSession.level.original
levelMajorVersion = @levelSession.level.majorVersion
sessionID = @clientResponseObject.originalSessionID
sessionTotalScore = @newScoresObject[sessionID].totalScore
opponentSessionID = _.pull(_.keys(@newScoresObject), sessionID)
opponentSessionTotalScore = @newScoresObject[opponentSessionID].totalScore
opposingTeam = calculateOpposingTeam(@clientResponseObject.originalSessionTeam)
try
levelOriginalID = @levelSession.level.original
levelMajorVersion = @levelSession.level.majorVersion
sessionID = @clientResponseObject.originalSessionID
sessionTotalScore = @newScoresObject[sessionID].totalScore
opponentSessionID = _.pull(_.keys(@newScoresObject), sessionID)
opponentSessionTotalScore = @newScoresObject[opponentSessionID].totalScore
opposingTeam = calculateOpposingTeam(@clientResponseObject.originalSessionTeam)
catch e
cb e
retrieveAllOpponentSessionIDs sessionID, (err, opponentSessionIDs) ->
if err? then return cb err, null
@ -708,7 +762,7 @@ hasTaskTimedOut = (taskSentTimestamp) -> taskSentTimestamp + scoringTaskTimeoutI
handleTimedOutTask = (req, res, taskBody) -> errors.clientTimeout res, "The results weren't provided within the timeout"
putRankingFromMetricsIntoScoreObject = (taskObject,scoreObject) ->
putRankingFromMetricsIntoScoreObject = (taskObject, scoreObject) ->
scoreObject = _.indexBy scoreObject, 'id'
scoreObject[session.sessionID].gameRanking = session.metrics.rank for session in taskObject.sessions
return scoreObject

View file

@ -22,7 +22,7 @@ productionLogging = (tokens, req, res) ->
else if status >= 300 then color = 36
elapsed = (new Date()) - req._startTime
elapsedColor = if elapsed < 500 then 90 else 31
if (status isnt 200 and status isnt 304 and status isnt 302) or elapsed > 500
if (status isnt 200 and status isnt 204 and status isnt 304 and status isnt 302) or elapsed > 500
return "\x1b[90m#{req.method} #{req.originalUrl} \x1b[#{color}m#{res.statusCode} \x1b[#{elapsedColor}m#{elapsed}ms\x1b[0m"
null
@ -92,7 +92,7 @@ sendMain = (req, res) ->
fs.readFile path.join(__dirname, 'public', 'main.html'), 'utf8', (err, data) ->
log.error "Error modifying main.html: #{err}" if err
# insert the user object directly into the html so the application can have it immediately. Sanitize </script>
data = data.replace('"userObjectTag"', JSON.stringify(UserHandler.formatEntity(req, req.user)).replace('/', '\\/'))
data = data.replace('"userObjectTag"', JSON.stringify(UserHandler.formatEntity(req, req.user)).replace(/\//g, '\\/'))
res.send data
setupFacebookCrossDomainCommunicationRoute = (app) ->