Merge branch 'feature/realtime-multiplayer'

This commit is contained in:
Nick Winter 2014-09-03 16:14:58 -07:00
commit 8f38d57aaa
22 changed files with 491 additions and 17 deletions

View file

@ -0,0 +1,6 @@
module.exports = class RealTimeCollection extends Backbone.Firebase.Collection
constructor: (savePath) ->
# TODO: Don't hard code this here
# TODO: Use prod path in prod
@firebase = 'https://codecombat.firebaseio.com/test/db/' + savePath
super()

View file

@ -7,6 +7,7 @@ channelSchemas =
'editor': require './schemas/subscriptions/editor'
'errors': require './schemas/subscriptions/errors'
'misc': require './schemas/subscriptions/misc'
'multiplayer': require './schemas/subscriptions/multiplayer'
'play': require './schemas/subscriptions/play'
'surface': require './schemas/subscriptions/surface'
'tome': require './schemas/subscriptions/tome'

View file

@ -1,4 +1,4 @@
Aether.addGlobal 'Vector', require 'lib/world/vector'
Aether.addGlobal 'Vector', require './world/vector'
Aether.addGlobal '_', _
module.exports.createAetherOptions = (options) ->

View file

@ -10,6 +10,7 @@ Letterbox = require './Letterbox'
Dimmer = require './Dimmer'
CountdownScreen = require './CountdownScreen'
PlaybackOverScreen = require './PlaybackOverScreen'
WaitingScreen = require './WaitingScreen'
DebugDisplay = require './DebugDisplay'
CoordinateDisplay = require './CoordinateDisplay'
CoordinateGrid = require './CoordinateGrid'
@ -67,6 +68,7 @@ module.exports = Surface = class Surface extends CocoClass
'level:set-letterbox': 'onSetLetterbox'
'application:idle-changed': 'onIdleChanged'
'camera:zoom-updated': 'onZoomUpdated'
'playback:real-time-playback-waiting': 'onRealTimePlaybackWaiting'
'playback:real-time-playback-started': 'onRealTimePlaybackStarted'
'playback:real-time-playback-ended': 'onRealTimePlaybackEnded'
'level:flag-color-selected': 'onFlagColorSelected'
@ -101,6 +103,7 @@ module.exports = Surface = class Surface extends CocoClass
@dimmer?.destroy()
@countdownScreen?.destroy()
@playbackOverScreen?.destroy()
@waitingScreen?.destroy()
@coordinateDisplay?.destroy()
@coordinateGrid?.destroy()
@stage.clear()
@ -402,6 +405,7 @@ module.exports = Surface = class Surface extends CocoClass
@spriteBoss = new SpriteBoss camera: @camera, surfaceLayer: @surfaceLayer, surfaceTextLayer: @surfaceTextLayer, world: @world, thangTypes: @options.thangTypes, choosing: @options.choosing, navigateToSelection: @options.navigateToSelection, showInvisible: @options.showInvisible
@countdownScreen = new CountdownScreen camera: @camera, layer: @screenLayer
@playbackOverScreen = new PlaybackOverScreen camera: @camera, layer: @screenLayer
@waitingScreen = new WaitingScreen camera: @camera, layer: @screenLayer
@initCoordinates()
@stage.enableMouseOver(10)
@stage.addEventListener 'stagemousemove', @onMouseMove
@ -577,7 +581,11 @@ module.exports = Surface = class Surface extends CocoClass
@stage.update e
# Real-time playback
onRealTimePlaybackWaiting: (e) ->
@onRealTimePlaybackStarted e
onRealTimePlaybackStarted: (e) ->
return if @realTime
@realTime = true
@onResize()
@spriteBoss.selfWizardSprite?.toggle false

View file

@ -0,0 +1,84 @@
CocoClass = require 'lib/CocoClass'
RealTimeCollection = require 'collections/RealTimeCollection'
module.exports = class WaitingScreen extends CocoClass
subscriptions:
'playback:real-time-playback-waiting': 'onRealTimePlaybackWaiting'
'playback:real-time-playback-started': 'onRealTimePlaybackStarted'
'playback:real-time-playback-ended': 'onRealTimePlaybackEnded'
'real-time-multiplayer:joined-game': 'onJoinedRealTimeMultiplayerGame'
'real-time-multiplayer:left-game': 'onLeftRealTimeMultiplayerGame'
constructor: (options) ->
super()
options ?= {}
@camera = options.camera
@layer = options.layer
@waitingText = options.text or 'Waiting...'
console.error @toString(), 'needs a camera.' unless @camera
console.error @toString(), 'needs a layer.' unless @layer
@build()
onCastingBegins: (e) -> @show() unless e.preload
onCastingEnds: (e) -> @hide()
toString: -> '<WaitingScreen>'
build: ->
@dimLayer = new createjs.Container()
@dimLayer.mouseEnabled = @dimLayer.mouseChildren = false
@dimLayer.addChild @dimScreen = new createjs.Shape()
@dimScreen.graphics.beginFill('rgba(0,0,0,0.5)').rect 0, 0, @camera.canvasWidth, @camera.canvasHeight
@dimLayer.alpha = 0
@dimLayer.addChild @makeWaitingText()
makeWaitingText: ->
size = Math.ceil @camera.canvasHeight / 8
text = new createjs.Text @waitingText, "#{size}px Bangers", '#F7B42C'
text.shadow = new createjs.Shadow '#000', Math.ceil(@camera.canvasHeight / 300), Math.ceil(@camera.canvasHeight / 300), Math.ceil(@camera.canvasHeight / 120)
text.textAlign = 'center'
text.textBaseline = 'middle'
text.x = @camera.canvasWidth / 2
text.y = @camera.canvasHeight / 2
@text = text
return text
show: ->
return if @showing
@showing = true
@dimLayer.alpha = 0
createjs.Tween.removeTweens @dimLayer
createjs.Tween.get(@dimLayer).to({alpha: 1}, 500)
@updateText()
@layer.addChild @dimLayer
hide: ->
return unless @showing
@showing = false
createjs.Tween.removeTweens @dimLayer
createjs.Tween.get(@dimLayer).to({alpha: 0}, 500).call => @layer.removeChild @dimLayer unless @destroyed
updateText: ->
if @multiplayerSession
players = new RealTimeCollection('multiplayer_level_sessions/' + @multiplayerSession.id + '/players')
players.each (player) =>
if player.id isnt me.id
name = player.get('name')
@text.text = "Waiting for #{name}..."
onRealTimePlaybackWaiting: (e) ->
@show()
onRealTimePlaybackStarted: (e) ->
@hide()
onRealTimePlaybackEnded: (e) ->
@hide()
onJoinedRealTimeMultiplayerGame: (e) ->
@multiplayerSession = e.session
onLeftRealTimeMultiplayerGame: (e) ->
if @multiplayerSession
@multiplayerSession.off()
@multiplayerSession = null

View file

@ -0,0 +1,6 @@
module.exports = class RealTimeModel extends Backbone.Firebase.Model
constructor: (savePath) ->
# TODO: Don't hard code this here
# TODO: Use prod path in prod
@firebase = 'https://codecombat.firebaseio.com/test/db/' + savePath
super()

View file

@ -0,0 +1,14 @@
c = require 'schemas/schemas'
module.exports =
'real-time-multiplayer:joined-game': c.object {title: 'Multiplayer joined game', required: ['session']},
session: {type: 'object'}
'real-time-multiplayer:left-game': c.object {title: 'Multiplayer left game'}
'real-time-multiplayer:manual-cast': c.object {title: 'Multiplayer manual cast'}
'real-time-multiplayer:new-opponent-code': c.object {title: 'Multiplayer new opponent code', required: ['code', 'codeLanguage']},
code: {type: 'object'}
codeLanguage: {type: 'string'}

View file

@ -85,6 +85,8 @@ module.exports =
'playback:stop-real-time-playback': c.object {}
'playback:real-time-playback-waiting': c.object {}
'playback:real-time-playback-started': c.object {}
'playback:real-time-playback-ended': c.object {}

View file

@ -39,7 +39,7 @@ module.exports = # /app/lib/surface
'sprite:speech-updated': c.object {required: ['sprite', 'thang']},
sprite: {type: 'object'}
thang: {type: ['object', 'null']}
blurb: {type: ['string', 'null']}
blurb: {type: ['string', 'null', 'undefined']}
message: {type: 'string'}
mood: {type: 'string'}
responses: {type: ['array', 'null', 'undefined']}
@ -47,7 +47,7 @@ module.exports = # /app/lib/surface
sound: {type: ['null', 'undefined', 'object']}
'level:sprite-dialogue': c.object {required: ['spriteID', 'message']},
blurb: {type: ['string', 'null']}
blurb: {type: ['string', 'null', 'undefined']}
message: {type: 'string'}
mood: {type: 'string'}
responses: {type: ['array', 'null', 'undefined']}

View file

@ -5,24 +5,86 @@ if !ladderGame
input#multiplayer(name="multiplayer", type="checkbox", checked=multiplayer)
span(data-i18n="multiplayer.multiplayer_toggle") Enable multiplayer
span.help-block(data-i18n="multiplayer.multiplayer_toggle_description") Allow others to join your game.
hr
div#link-area
p(data-i18n="multiplayer.multiplayer_link_description") Give this link to anyone to have them join you.
textarea.well#multiplayer-join-link(readonly=true)= joinLink
p
strong(data-i18n="multiplayer.multiplayer_hint_label") Hint:
span(data-i18n="multiplayer.multiplayer_hint") Click the link to select all, then press ⌘-C or Ctrl-C to copy the link.
p(data-i18n="multiplayer.multiplayer_coming_soon") More multiplayer features to come!
if ladderGame
if me.get('anonymous')
p(data-i18n="multiplayer.multiplayer_sign_in_leaderboard") Sign in or create an account and get your solution on the leaderboard.
else if readyToRank
button#create-game-button Create Game
hr
div#created-multiplayer-session
h3 Your Game
if currentMultiplayerSession
div
span(style="margin:10px")= currentMultiplayerSession.get('levelID')
span(style="margin:10px")= currentMultiplayerSession.get('creatorName')
span(style="margin:10px")= currentMultiplayerSession.get('state')
span(style="margin:10px")= currentMultiplayerSession.id
button#leave-game-button(data-item=item) Leave Game
div
- var players = playersCollections[currentMultiplayerSession.id]
span(style="margin:10px") Players:
- for (var i=0; i < players.length; i++) {
- var name = players.at(i).get('name')
- var team = players.at(i).get('team')
span(style="margin:10px")= name
span(style="margin:10px")= team
- }
else
div Click something above to create a game.
hr
div#open-games
h3 Open Games
// TODO: do not let you join ones with same-team opponent
- var noOpenGames = true
if multiplayerSessions
- for (var i=0; i < multiplayerSessions.length; i++) {
if levelID === multiplayerSessions[i].get('levelID') && multiplayerSessions[i].get('state') === 'creating'
- var id = multiplayerSessions[i].get('id')
- var players = playersCollections[id]
if players && players.length < 2
- noOpenGames = false
- var creatorName = multiplayerSessions[i].get('creatorName')
- var creator = multiplayerSessions[i].get('creator')
- var state = multiplayerSessions[i].get('state')
- var item = multiplayerSessions[i]
div
button#join-game-button(data-item=item) Join Game
span(style="margin:10px")= levelID
span(style="margin:10px")= creatorName
span(style="margin:10px")= state
span(style="margin:10px")= id
div
span(style="margin:10px") Players:
- for (var j=0; j < players.length; j++) {
- var name = players.at(j).get('name')
- var team = players.at(j).get('team')
span(style="margin:10px")= name
span(style="margin:10px")= team
- }
- }
if noOpenGames
div No games available.
hr
.ladder-submission-view
else
a.btn.btn-primary(href="/play/ladder/#{levelSlug}#my-matches", data-i18n="multiplayer.victory_go_ladder") Return to Ladder

View file

@ -1,13 +1,24 @@
h4.home
a(href=homeLink || "/")
a(href=homeLink || "/")
i.icon-home.icon-white
span(data-i18n="play_level.home") Home
h4.title
| #{worldName}
| -
span.spl.spr -
a(href=editorLink, data-i18n="nav.editor", title="Open " + worldName + " in the Level Editor") Editor
if multiplayerSession
- found = false
- for (var i=0; i < multiplayerPlayers.length; i++) {
if (multiplayerPlayers.at(i).id !== meID)
span.spl.spr - vs #{multiplayerPlayers.at(i).get('name')}
- found = true
- break
- }
if !found
span.spl.spr - waiting...
button.btn.btn-xs.btn-warning.banner#stop-real-time-playback-button(title="Stop real-time playback", data-i18n="play_level.stop") Stop

View file

@ -240,7 +240,7 @@ module.exports = class InventoryView extends CocoView
onHidden: ->
inventory = @getCurrentEquipmentConfig()
heroConfig = @options.session.get('heroConfig') ? {}
return if _.isEqual inventory, heroConfig.inventory
return if _.isEqual inventory, (heroConfig.inventory ? {})
heroConfig.inventory = inventory
heroConfig.thangType ?= '529ffbf1cf1818f2be000001' # Temp: assign Tharin as the hero
@options.session.set 'heroConfig', heroConfig

View file

@ -3,6 +3,7 @@ template = require 'templates/game-menu/multiplayer-view'
{me} = require 'lib/auth'
ThangType = require 'models/ThangType'
LadderSubmissionView = require 'views/play/common/LadderSubmissionView'
RealTimeCollection = require 'collections/RealTimeCollection'
module.exports = class MultiplayerView extends CocoView
id: 'multiplayer-view'
@ -15,6 +16,9 @@ module.exports = class MultiplayerView extends CocoView
events:
'click textarea': 'onClickLink'
'change #multiplayer': 'updateLinkSection'
'click #create-game-button': 'onCreateGame'
'click #join-game-button': 'onJoinGame'
'click #leave-game-button': 'onLeaveGame'
constructor: (options) ->
super(options)
@ -22,6 +26,13 @@ module.exports = class MultiplayerView extends CocoView
@session = options.session
@playableTeams = options.playableTeams
@listenTo @session, 'change:multiplayer', @updateLinkSection
@initMultiplayerSessions()
destroy: ->
@multiplayerSessions?.off()
@currentMultiplayerSession?.off()
collection.off() for id, collection of @playersCollections
super()
getRenderData: ->
c = super()
@ -34,6 +45,11 @@ module.exports = class MultiplayerView extends CocoView
if @level?.get('type') is 'ladder'
c.ladderGame = true
c.readyToRank = @session?.readyToRank()
c.levelID = @session.get('levelID')
c.multiplayerSessions = @multiplayerSessions.models
c.currentMultiplayerSession = @currentMultiplayerSession if @currentMultiplayerSession
c.playersCollections = @playersCollections if @playersCollections
c
afterRender: ->
@ -41,6 +57,8 @@ module.exports = class MultiplayerView extends CocoView
@updateLinkSection()
@ladderSubmissionView = new LadderSubmissionView session: @session, level: @level
@insertSubView @ladderSubmissionView, @$el.find('.ladder-submission-view')
@$el.find('#created-multiplayer-session').toggle Boolean(@currentMultiplayerSession?)
@$el.find('#create-game-button').toggle Boolean(not (@currentMultiplayerSession?))
onClickLink: (e) ->
e.target.select()
@ -52,9 +70,105 @@ module.exports = class MultiplayerView extends CocoView
updateLinkSection: ->
multiplayer = @$el.find('#multiplayer').prop('checked')
la = @$el.find('#link-area')
la.toggle Boolean(multiplayer)
la.toggle if @level?.get('type') is 'ladder' then false else Boolean(multiplayer)
true
onHidden: ->
multiplayer = Boolean(@$el.find('#multiplayer').prop('checked'))
@session.set('multiplayer', multiplayer)
# TODO: shouldn't have to open MultiplayerView to read existing multiplayerSession?
# TODO: if someone leaves your game, it should go back to 'creating' state
initMultiplayerSessions: ->
@playersCollections = {}
# TODO: only request sessions for this level, !team, etc.
# TODO: don't hard code this path all over the place
@multiplayerSessions = new RealTimeCollection('multiplayer_level_sessions/')
@multiplayerSessions.on 'add', @onMultiplayerSessionAdded
@multiplayerSessions.on 'remove', @onMultiplayerSessionRemoved
@multiplayerSessions.each (ms) => @initMultiplayerSession ms
initMultiplayerSession: (ms) ->
# TODO: double check these players events are needed on top of onMultiplayerSessionChanged
@playersCollections[ms.id] = new RealTimeCollection('multiplayer_level_sessions/' + ms.id + '/players')
@playersCollections[ms.id].on 'add', @onPlayerAdded
@playersCollections[ms.id].on 'remove', @onPlayerRemoved
if not @currentMultiplayerSession and ms.get('levelID') is @session.get('levelID')
@playersCollections[ms.id].each (player) =>
if player.id is me.id and player.get('team') is @session.get('team')
@currentMultiplayerSession = ms
@currentMultiplayerSession.on 'change', @onMultiplayerSessionChanged
Backbone.Mediator.publish 'real-time-multiplayer:joined-game', session: @currentMultiplayerSession
onMultiplayerSessionAdded: (e) =>
console.log 'onMultiplayerSessionAdded', e
@initMultiplayerSession e
@render()
onMultiplayerSessionRemoved: (e) =>
@playersCollections[e.id].off()
delete @playersCollections[e.id]
@render()
onMultiplayerSessionChanged: (e) =>
@render()
onPlayerAdded: (e) =>
# TODO: listeners not being unhooked
@render?()
onPlayerRemoved: (e) =>
# TODO: listeners not being unhooked
@render?()
onCreateGame: ->
s = @multiplayerSessions.create {
creator: @session.get('creator')
creatorName: @session.get('creatorName')
levelID: @session.get('levelID')
created: (new Date()).toISOString()
state: 'creating'
}
@currentMultiplayerSession = @multiplayerSessions.get(s.id)
@currentMultiplayerSession.on 'change', @onMultiplayerSessionChanged
players = new RealTimeCollection('multiplayer_level_sessions/' + @currentMultiplayerSession.id + '/players')
players.create {id: me.id, name: @session.get('creatorName'), team: @session.get('team')}
Backbone.Mediator.publish 'real-time-multiplayer:joined-game', session: @currentMultiplayerSession
@render()
onJoinGame: (e) ->
return if @currentMultiplayerSession
item = @$el.find(e.target).data('item')
@currentMultiplayerSession = @multiplayerSessions.get(item.id)
@currentMultiplayerSession.on 'change', @onMultiplayerSessionChanged
if @playersCollections[item.id]
@playersCollections[item.id].create {id: me.id, name: @session.get('creatorName'), team: @session.get('team')}
else
console.error 'onJoinGame did not have a players collection', @currentMultiplayerSession
Backbone.Mediator.publish 'real-time-multiplayer:joined-game', session: @currentMultiplayerSession
if @playersCollections[item.id]?.length is 2
@currentMultiplayerSession.set 'state', 'coding'
# TODO: close multiplayer view?
@render()
onLeaveGame: (e) ->
# TODO: This doesn't update open games or current game
if @currentMultiplayerSession
players = @playersCollections[@currentMultiplayerSession.id]
for i in [0...players.length]
player = players.at(i)
if player.get('id') is me.id
players.remove(player)
# NOTE: remove(@something) doesn't stick locally, only remotely
cms = @currentMultiplayerSession
@currentMultiplayerSession.off()
@currentMultiplayerSession = null
if players.length is 0
@multiplayerSessions.remove(cms)
break
console.error "Tried to leave a game we hadn't joined!" if @currentMultiplayerSession
Backbone.Mediator.publish 'real-time-multiplayer:left-game', {}
else
console.error "Tried to leave a game with no currentMultiplayerSession"
@render()

View file

@ -59,7 +59,7 @@ module.exports = class ModelModal extends ModalView
model.set key, val
for key, val of model.attributes when treema.get(key) is undefined and not _.string.startsWith key, '_'
console.log 'Deleting', key, 'which was', val, 'but man, that ain\'t going to work, now is it?'
model.unset key
#model.unset key
if errors = model.validate()
return console.warn model, 'failed validation with errors:', errors
return unless res = model.patch()
@ -72,4 +72,4 @@ module.exports = class ModelModal extends ModalView
destroy: ->
@modelTreemas[model].destroy() for model of @modelTreemas
super()
super()

View file

@ -1,8 +1,10 @@
CocoView = require 'views/kinds/CocoView'
template = require 'templates/play/level/control_bar'
{me} = require 'lib/auth'
LevelGuideModal = require './modal/LevelGuideModal'
GameMenuModal = require 'views/game-menu/GameMenuModal'
RealTimeCollection = require 'collections/RealTimeCollection'
module.exports = class ControlBarView extends CocoView
id: 'control-bar-view'
@ -10,6 +12,8 @@ module.exports = class ControlBarView extends CocoView
subscriptions:
'bus:player-states-changed': 'onPlayerStatesChanged'
'real-time-multiplayer:joined-game': 'onJoinedRealTimeMultiplayerGame'
'real-time-multiplayer:left-game': 'onLeftRealTimeMultiplayerGame'
events:
'click #docs-button': ->
@ -55,6 +59,9 @@ module.exports = class ControlBarView extends CocoView
else
c.homeLink = '/'
c.editorLink = "/editor/level/#{@level.get('slug')}"
c.multiplayerSession = @multiplayerSession if @multiplayerSession
c.multiplayerPlayers = @multiplayerPlayers if @multiplayerPlayers
c.meID = me.id
c
afterRender: ->
@ -77,3 +84,22 @@ module.exports = class ControlBarView extends CocoView
showGameMenuModal: ->
@openModalView new GameMenuModal level: @level, session: @session, playableTeams: @playableTeams
onJoinedRealTimeMultiplayerGame: (e) ->
@multiplayerSession = e.session
@multiplayerPlayers = new RealTimeCollection('multiplayer_level_sessions/' + @multiplayerSession.id + '/players')
@multiplayerPlayers.on 'add', @onRealTimeMultiplayerPlayerAdded
@multiplayerPlayers.on 'remove', @onRealTimeMultiplayerPlayerRemoved
@render()
onLeftRealTimeMultiplayerGame: (e) ->
@multiplayerSession = null
@multiplayerPlayers.off()
@multiplayerPlayers = null
@render()
onRealTimeMultiplayerPlayerAdded: (e) =>
@render() unless @destroyed
onRealTimeMultiplayerPlayerRemoved: (e) =>
@render() unless @destroyed

View file

@ -1,6 +1,7 @@
CocoView = require 'views/kinds/CocoView'
template = require 'templates/play/level/level-flags-view'
{me} = require 'lib/auth'
RealTimeCollection = require 'collections/RealTimeCollection'
module.exports = class LevelFlagsView extends CocoView
id: 'level-flags-view'
@ -13,6 +14,7 @@ module.exports = class LevelFlagsView extends CocoView
'god:new-world-created': 'onNewWorld'
'god:streaming-world-updated': 'onNewWorld'
'surface:remove-flag': 'onRemoveFlag'
'real-time-multiplayer:joined-game': 'onJoinedMultiplayerGame'
events:
'click .green-flag': -> @onFlagSelected color: 'green', source: 'button'
@ -55,6 +57,7 @@ module.exports = class LevelFlagsView extends CocoView
flag = player: me.id, team: me.team, color: @flagColor, pos: pos, time: @world.dt * @world.frames.length, active: true
@flags[@flagColor] = flag
@flagHistory.push flag
@realTimeFlags?.create flag
Backbone.Mediator.publish 'level:flag-updated', flag
#console.log 'trying to place flag at', @world.age, 'and think it will happen by', flag.time
@ -72,3 +75,26 @@ module.exports = class LevelFlagsView extends CocoView
onNewWorld: (event) ->
return unless event.world.name is @world.name
@world = @options.world = event.world
onJoinedMultiplayerGame: (e) ->
@realTimeFlags = new RealTimeCollection('multiplayer_level_sessions/' + e.session.id + '/flagHistory')
@realTimeFlags.on 'add', @onRealTimeMultiplayerFlagAdded
onLeftMultiplayerGame: (e) ->
if @realTimeFlags
@realTimeFlags.off()
@realTimeFlags = null
onRealTimeMultiplayerFlagAdded: (e) =>
if e.get('player') != me.id
# TODO: what is @flags used for?
# Build local flag from Backbone.Model flag
flag =
player: e.get('player')
team: e.get('team')
color: e.get('color')
pos: e.get('pos')
time: e.get('time')
active: e.get('active')
@flagHistory.push flag
Backbone.Mediator.publish 'level:flag-updated', flag

View file

@ -25,6 +25,7 @@ module.exports = class LevelPlaybackView extends CocoView
'tome:cast-spells': 'onTomeCast'
'playback:real-time-playback-ended': 'onRealTimePlaybackEnded'
'playback:stop-real-time-playback': 'onStopRealTimePlayback'
'real-time-multiplayer:manual-cast': 'onRealTimeMultiplayerCast'
events:
'click #debug-toggle': 'onToggleDebug'
@ -165,6 +166,11 @@ module.exports = class LevelPlaybackView extends CocoView
@togglePlaybackControls false
Backbone.Mediator.publish 'playback:real-time-playback-started', {}
onRealTimeMultiplayerCast: (e) ->
@realTime = true
@togglePlaybackControls false
Backbone.Mediator.publish 'playback:real-time-playback-waiting', {}
onWindowResize: (s...) =>
@barWidth = $('.progress', @$el).width()

View file

@ -18,6 +18,8 @@ LevelComponent = require 'models/LevelComponent'
Article = require 'models/Article'
Camera = require 'lib/surface/Camera'
AudioPlayer = require 'lib/AudioPlayer'
RealTimeModel = require 'models/RealTimeModel'
RealTimeCollection = require 'collections/RealTimeCollection'
# subviews
LevelLoadingView = require './LevelLoadingView'
@ -62,8 +64,12 @@ module.exports = class PlayLevelView extends RootView
'level:session-will-save': 'onSessionWillSave'
'level:started': 'onLevelStarted'
'level:loading-view-unveiled': 'onLoadingViewUnveiled'
'playback:real-time-playback-waiting': 'onRealTimePlaybackWaiting'
'playback:real-time-playback-started': 'onRealTimePlaybackStarted'
'playback:real-time-playback-ended': 'onRealTimePlaybackEnded'
'real-time-multiplayer:joined-game': 'onJoinedRealTimeMultiplayerGame'
'real-time-multiplayer:left-game': 'onLeftRealTimeMultiplayerGame'
'real-time-multiplayer:manual-cast': 'onRealTimeMultiplayerCast'
'level:inventory-changed': 'onInventoryChanged'
events:
@ -553,6 +559,10 @@ module.exports = class PlayLevelView extends RootView
AudioPlayer.preloadSoundReference sound
# Real-time playback
onRealTimePlaybackWaiting: (e) ->
@$el.addClass('real-time').focus()
@onWindowResize()
onRealTimePlaybackStarted: (e) ->
@$el.addClass('real-time').focus()
@onWindowResize()
@ -560,6 +570,7 @@ module.exports = class PlayLevelView extends RootView
onRealTimePlaybackEnded: (e) ->
@$el.removeClass 'real-time'
@onWindowResize()
@onRealTimeMultiplayerPlaybackEnded()
destroy: ->
@levelLoader?.destroy()
@ -575,3 +586,66 @@ module.exports = class PlayLevelView extends RootView
delete window.nextLevelURL
console.profileEnd?() if PROFILE_ME
super()
# Real-time Multiplayer ######################################################
onRealTimeMultiplayerPlaybackEnded: ->
if @multiplayerSession
@multiplayerSession.set 'state', 'coding'
players = new RealTimeCollection('multiplayer_level_sessions/' + @multiplayerSession.id + '/players')
players.each (player) -> player.set 'state', 'coding' if player.id is me.id
onJoinedRealTimeMultiplayerGame: (e) ->
@multiplayerSession = new RealTimeModel('multiplayer_level_sessions/' + e.session.id)
onLeftRealTimeMultiplayerGame: (e) ->
if @multiplayerSession
@multiplayerSession.off()
@multiplayerSession = null
onRealTimeMultiplayerCast: (e) ->
unless @multiplayerSession
console.error 'onRealTimeMultiplayerCast without a multiplayerSession'
return
players = new RealTimeCollection('multiplayer_level_sessions/' + @multiplayerSession.id + '/players')
myPlayer = opponentPlayer = null
players.each (player) ->
if player.id is me.id
myPlayer = player
else
opponentPlayer = player
if myPlayer
console.info 'Submitting my code'
myPlayer.set 'code', @session.get('code')
myPlayer.set 'codeLanguage', @session.get('codeLanguage')
myPlayer.set 'state', 'submitted'
else
console.error 'Did not find my player in onRealTimeMultiplayerCast'
if opponentPlayer
# TODO: Shouldn't need nested opponentPlayer change listeners here
state = opponentPlayer.get('state')
console.info 'Other player is', state
if state in ['submitted', 'ready']
@onOpponentSubmitted(opponentPlayer, myPlayer)
else
# Wait for opponent to submit their code
opponentPlayer.on 'change', (e) =>
state = opponentPlayer.get('state')
if state in ['submitted', 'ready']
@onOpponentSubmitted(opponentPlayer, myPlayer)
onOpponentSubmitted: (opponentPlayer, myPlayer) =>
# Save opponent's code
Backbone.Mediator.publish 'real-time-multiplayer:new-opponent-code', {codeLanguage: opponentPlayer.get('codeLanguage'), code: opponentPlayer.get('code')}
# I'm ready to rumble
myPlayer.set 'state', 'ready'
if opponentPlayer.get('state') is 'ready'
console.info 'All real-time multiplayer players are ready!'
@multiplayerSession.set 'state', 'running'
else
# Wait for opponent to be ready
opponentPlayer.on 'change', (e) =>
if opponentPlayer.get('state') is 'ready'
opponentPlayer.off()
console.info 'All real-time multiplayer players are ready!'
@multiplayerSession.set 'state', 'running'

View file

@ -15,6 +15,8 @@ module.exports = class CastButtonView extends CocoView
'tome:cast-spells': 'onCastSpells'
'god:world-load-progress-changed': 'onWorldLoadProgressChanged'
'god:new-world-created': 'onNewWorld'
'real-time-multiplayer:joined-game': 'onJoinedRealTimeMultiplayerGame'
'real-time-multiplayer:left-game': 'onLeftRealTimeMultiplayerGame'
constructor: (options) ->
super options
@ -48,7 +50,16 @@ module.exports = class CastButtonView extends CocoView
Backbone.Mediator.publish 'tome:manual-cast', {}
onCastRealTimeButtonClick: (e) ->
Backbone.Mediator.publish 'tome:manual-cast', {realTime: true}
if @multiplayerSession
Backbone.Mediator.publish 'real-time-multiplayer:manual-cast', {}
# Wait for multiplayer session to be up and running
@multiplayerSession.on 'change', (e) =>
if @multiplayerSession.get('state') is 'running'
# Real-time multiplayer session is ready to go, so resume normal cast
@multiplayerSession.off()
Backbone.Mediator.publish 'tome:manual-cast', {realTime: true}
else
Backbone.Mediator.publish 'tome:manual-cast', {realTime: true}
onCastOptionsClick: (e) =>
Backbone.Mediator.publish 'tome:focus-editor', {}
@ -106,3 +117,11 @@ module.exports = class CastButtonView extends CocoView
spell.view?.setAutocastDelay delay for spellKey, spell of @spells
@castOptions.find('a').each ->
$(@).toggleClass('selected', parseInt($(@).attr('data-delay')) is delay)
onJoinedRealTimeMultiplayerGame: (e) ->
@multiplayerSession = e.session
onLeftRealTimeMultiplayerGame: (e) ->
if @multiplayerSession
@multiplayerSession.off()
@multiplayerSession = null

View file

@ -45,6 +45,7 @@ module.exports = class Spell
@tabView.render()
@team = @permissions.readwrite[0] ? 'common'
Backbone.Mediator.publish 'tome:spell-created', spell: @
Backbone.Mediator.subscribe 'real-time-multiplayer:new-opponent-code', @onNewOpponentCode
destroy: ->
@view?.destroy()
@ -161,3 +162,13 @@ module.exports = class Spell
# Players without permissions can't view the raw code.
return true if @session.get('creator') isnt me.id and not (me.isAdmin() or 'employer' in me.get('permissions', true))
false
onNewOpponentCode: (e) =>
return unless @spellKey
if e.codeLanguage and e.code
spellkeyComponents = @spellKey.split '/'
if e.code[spellkeyComponents[0]]?[spellkeyComponents[1]]
@source = e.code[spellkeyComponents[0]][spellkeyComponents[1]]
@updateLanguageAether e.codeLanguage
else
console.error 'Spell onNewOpponentCode did not recieve code', e

View file

@ -45,12 +45,16 @@
"validated-backbone-mediator": "~0.1.3",
"jquery.browser": "~0.0.6",
"zatanna": "~0.0.6",
"modernizr": "~2.8.3"
"modernizr": "~2.8.3",
"backfire": "~0.3.0"
},
"overrides": {
"backbone": {
"main": "backbone.js"
},
"backfire": {
"main": "backbone-firebase.min.js"
},
"lodash": {
"main": "dist/lodash.js"
},

View file

@ -2,7 +2,7 @@ do (setupLodash = this) ->
GLOBAL._ = require 'lodash'
_.str = require 'underscore.string'
_.mixin _.str.exports()
Aether = require 'aether'
GLOBAL.Aether = Aether = require 'aether'
async = require 'async'
serverSetup = require '../server_setup'