mirror of
https://github.com/codeninjasllc/codecombat.git
synced 2025-03-14 07:00:01 -04:00
Merge branch 'feature/realtime-multiplayer'
This commit is contained in:
commit
8f38d57aaa
22 changed files with 491 additions and 17 deletions
6
app/collections/RealTimeCollection.coffee
Normal file
6
app/collections/RealTimeCollection.coffee
Normal 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()
|
|
@ -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'
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
Aether.addGlobal 'Vector', require 'lib/world/vector'
|
||||
Aether.addGlobal 'Vector', require './world/vector'
|
||||
Aether.addGlobal '_', _
|
||||
|
||||
module.exports.createAetherOptions = (options) ->
|
||||
|
|
|
@ -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
|
||||
|
|
84
app/lib/surface/WaitingScreen.coffee
Normal file
84
app/lib/surface/WaitingScreen.coffee
Normal 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
|
6
app/models/RealTimeModel.coffee
Normal file
6
app/models/RealTimeModel.coffee
Normal 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()
|
14
app/schemas/subscriptions/multiplayer.coffee
Normal file
14
app/schemas/subscriptions/multiplayer.coffee
Normal 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'}
|
||||
|
|
@ -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 {}
|
||||
|
|
|
@ -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']}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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()
|
||||
|
||||
|
|
|
@ -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'
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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"
|
||||
},
|
||||
|
|
|
@ -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'
|
||||
|
|
Loading…
Reference in a new issue