Merge branch 'master' into production

This commit is contained in:
Nick Winter 2014-11-17 21:30:57 -08:00
commit 2574f85f3e
34 changed files with 788 additions and 264 deletions

View file

@ -29,7 +29,7 @@ elementAcceptsKeystrokes = (el) ->
# not radio, checkbox, range, or color # not radio, checkbox, range, or color
return (tag is 'textarea' or (tag is 'input' and type in textInputTypes) or el.contentEditable in ['', 'true']) and not (el.readOnly or el.disabled) return (tag is 'textarea' or (tag is 'input' and type in textInputTypes) or el.contentEditable in ['', 'true']) and not (el.readOnly or el.disabled)
COMMON_FILES = ['/images/pages/base/modal_background.png', '/images/level/code_palette_background.png', '/images/level/popover_background.png', '/images/level/code_editor_background.png'] COMMON_FILES = ['/images/pages/base/modal_background.png', '/images/level/code_palette_wood_background.png', '/images/level/popover_background.png', '/images/level/code_editor_background.png']
preload = (arrayOfImages) -> preload = (arrayOfImages) ->
$(arrayOfImages).each -> $(arrayOfImages).each ->
$('<img/>')[0].src = @ $('<img/>')[0].src = @

View file

@ -392,20 +392,29 @@ self.onWorldLoaded = function onWorldLoaded() {
self.postMessage({type: 'end-load-frames', goalStates: goalStates, overallStatus: overallStatus}); self.postMessage({type: 'end-load-frames', goalStates: goalStates, overallStatus: overallStatus});
var t1 = new Date(); var t1 = new Date();
var diff = t1 - self.t0; var diff = t1 - self.t0;
if (self.world.headless) if(self.world.headless)
return console.log('Headless simulation completed in ' + diff + 'ms.'); return console.log('Headless simulation completed in ' + diff + 'ms.');
var worldEnded = self.world.ended;
var totalFrames = self.world.totalFrames;
var transferableSupported = self.transferableSupported(); var transferableSupported = self.transferableSupported();
try { try {
var serialized = self.world.serialize(); var serialized = self.world.serialize();
} }
catch(error) { catch(error) {
console.log("World serialization error:", error.toString() + "\n" + error.stack || error.stackTrace); console.log("World serialization error:", error.toString() + "\n" + error.stack || error.stackTrace);
self.destroyWorld();
return;
} }
//self.serialized = serialized; // Testing peak memory usage
//return; // Testing peak memory usage
if(worldEnded)
// Make sure we clean up memory as soon as possible, since we just used the most ever and don't want to crash.
self.destroyWorld();
var t2 = new Date(); var t2 = new Date();
//console.log("About to transfer", serialized.serializedWorld.trackedPropertiesPerThangValues, serialized.transferableObjects); //console.log("About to transfer", serialized.serializedWorld.trackedPropertiesPerThangValues, serialized.transferableObjects);
var messageType = self.world.ended ? 'new-world' : 'some-frames-serialized'; var messageType = worldEnded ? 'new-world' : 'some-frames-serialized';
try { try {
var message = {type: messageType, serialized: serialized.serializedWorld, goalStates: goalStates, startFrame: serialized.startFrame, endFrame: serialized.endFrame}; var message = {type: messageType, serialized: serialized.serializedWorld, goalStates: goalStates, startFrame: serialized.startFrame, endFrame: serialized.endFrame};
if(transferableSupported) if(transferableSupported)
@ -417,15 +426,18 @@ self.onWorldLoaded = function onWorldLoaded() {
console.log("World delivery error:", error.toString() + "\n" + error.stack || error.stackTrace); console.log("World delivery error:", error.toString() + "\n" + error.stack || error.stackTrace);
} }
if(self.world.ended) { if(worldEnded) {
var t3 = new Date(); 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"); console.log("And it was so: (" + (diff / totalFrames).toFixed(3) + "ms per frame,", totalFrames, "frames)\nSimulation :", diff + "ms \nSerialization:", (t2 - t1) + "ms\nDelivery :", (t3 - t2) + "ms");
self.world.goalManager.destroy();
self.world.destroy();
self.world = null;
} }
}; };
self.destroyWorld = function destroyWorld() {
self.world.goalManager.destroy();
self.world.destroy();
self.world = null;
};
self.onWorldPreloaded = function onWorldPreloaded() { self.onWorldPreloaded = function onWorldPreloaded() {
self.goalManager.worldGenerationEnded(); self.goalManager.worldGenerationEnded();
var goalStates = self.goalManager.getGoalStates(); var goalStates = self.goalManager.getGoalStates();

View file

@ -35,7 +35,6 @@ module.exports = Bus = class Bus extends CocoClass
Backbone.Mediator.publish 'bus:connected', {bus: @} Backbone.Mediator.publish 'bus:connected', {bus: @}
disconnect: -> disconnect: ->
Firebase.goOffline()
@fireRef?.off() @fireRef?.off()
@fireRef = null @fireRef = null
@fireChatRef?.off() @fireChatRef?.off()

View file

@ -56,7 +56,6 @@ module.exports = class LevelLoader extends CocoClass
onLevelLoaded: -> onLevelLoaded: ->
@loadSession() @loadSession()
@populateLevel() @populateLevel()
Backbone.Mediator.publish 'level:loaded', level: @level, team: @team ? 'humans'
# Session Loading # Session Loading
@ -72,7 +71,7 @@ module.exports = class LevelLoader extends CocoClass
@session = @sessionResource.model @session = @sessionResource.model
if @opponentSessionID if @opponentSessionID
opponentSession = new LevelSession().setURL "/db/level.session/#{@opponentSessionID}" opponentSession = new LevelSession().setURL "/db/level.session/#{@opponentSessionID}"
@opponentSessionResource = @supermodel.loadModel(opponentSession, 'opponent_session') @opponentSessionResource = @supermodel.loadModel(opponentSession, 'opponent_session', {cache: false})
@opponentSession = @opponentSessionResource.model @opponentSession = @opponentSessionResource.model
if @session.loaded if @session.loaded
@ -90,6 +89,9 @@ module.exports = class LevelLoader extends CocoClass
loadDependenciesForSession: (session) -> loadDependenciesForSession: (session) ->
if session is @session if session is @session
# hero-ladder games require the correct session team in level:loaded
team = @team ? @session.get('team')
Backbone.Mediator.publish 'level:loaded', level: @level, team: team
Backbone.Mediator.publish 'level:session-loaded', level: @level, session: @session Backbone.Mediator.publish 'level:session-loaded', level: @level, session: @session
@consolidateFlagHistory() if @opponentSession?.loaded @consolidateFlagHistory() if @opponentSession?.loaded
else if session is @opponentSession else if session is @opponentSession

View file

@ -0,0 +1,10 @@
publishableKey = if application.isProduction() then 'pk_live_27jQZozjDGN1HSUTnSuM578g' else 'pk_test_zG5UwVu6Ww8YhtE9ZYh0JO6a'
module.exports = handler = StripeCheckout.configure({
key: publishableKey
name: 'CodeCombat'
email: me.get('email')
image: '/images/pages/base/logo_square_250.png'
token: (token) ->
Backbone.Mediator.publish 'stripe:received-token', { token: token }
})

View file

@ -6,8 +6,7 @@ module.exports = class WaitingScreen extends CocoClass
'playback:real-time-playback-waiting': 'onRealTimePlaybackWaiting' 'playback:real-time-playback-waiting': 'onRealTimePlaybackWaiting'
'playback:real-time-playback-started': 'onRealTimePlaybackStarted' 'playback:real-time-playback-started': 'onRealTimePlaybackStarted'
'playback:real-time-playback-ended': 'onRealTimePlaybackEnded' 'playback:real-time-playback-ended': 'onRealTimePlaybackEnded'
'real-time-multiplayer:joined-game': 'onJoinedRealTimeMultiplayerGame' 'real-time-multiplayer:player-status': 'onRealTimeMultiplayerPlayerStatus'
'real-time-multiplayer:left-game': 'onLeftRealTimeMultiplayerGame'
constructor: (options) -> constructor: (options) ->
super() super()
@ -49,7 +48,6 @@ module.exports = class WaitingScreen extends CocoClass
@dimLayer.alpha = 0 @dimLayer.alpha = 0
createjs.Tween.removeTweens @dimLayer createjs.Tween.removeTweens @dimLayer
createjs.Tween.get(@dimLayer).to({alpha: 1}, 500) createjs.Tween.get(@dimLayer).to({alpha: 1}, 500)
@updateText()
@layer.addChild @dimLayer @layer.addChild @dimLayer
hide: -> hide: ->
@ -58,27 +56,10 @@ module.exports = class WaitingScreen extends CocoClass
createjs.Tween.removeTweens @dimLayer createjs.Tween.removeTweens @dimLayer
createjs.Tween.get(@dimLayer).to({alpha: 0}, 500).call => @layer.removeChild @dimLayer unless @destroyed createjs.Tween.get(@dimLayer).to({alpha: 0}, 500).call => @layer.removeChild @dimLayer unless @destroyed
updateText: -> onRealTimeMultiplayerPlayerStatus: (e) -> @text.text = e.status
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) -> onRealTimePlaybackWaiting: (e) -> @show()
@show()
onRealTimePlaybackStarted: (e) -> onRealTimePlaybackStarted: (e) -> @hide()
@hide()
onRealTimePlaybackEnded: (e) -> onRealTimePlaybackEnded: (e) -> @hide()
@hide()
onJoinedRealTimeMultiplayerGame: (e) ->
@multiplayerSession = e.session
onLeftRealTimeMultiplayerGame: (e) ->
if @multiplayerSession
@multiplayerSession.off()
@multiplayerSession = null

View file

@ -17,6 +17,7 @@ REAL_TIME_BUFFER_MAX = 3 * PROGRESS_UPDATE_INTERVAL
REAL_TIME_BUFFERED_WAIT_INTERVAL = 0.5 * PROGRESS_UPDATE_INTERVAL REAL_TIME_BUFFERED_WAIT_INTERVAL = 0.5 * PROGRESS_UPDATE_INTERVAL
REAL_TIME_COUNTDOWN_DELAY = 3000 # match CountdownScreen REAL_TIME_COUNTDOWN_DELAY = 3000 # match CountdownScreen
ITEM_ORIGINAL = '53e12043b82921000051cdf9' ITEM_ORIGINAL = '53e12043b82921000051cdf9'
EXISTS_ORIGINAL = '524b4150ff92f1f4f8000024'
COUNTDOWN_LEVELS = ['sky-span'] COUNTDOWN_LEVELS = ['sky-span']
module.exports = class World module.exports = class World
@ -237,13 +238,19 @@ module.exports = class World
loadThangFromLevel: (thangConfig, levelComponents, thangTypes, equipBy=null) -> loadThangFromLevel: (thangConfig, levelComponents, thangTypes, equipBy=null) ->
components = [] components = []
for component in thangConfig.components for component, componentIndex in thangConfig.components
componentModel = _.find levelComponents, (c) -> componentModel = _.find levelComponents, (c) ->
c.original is component.original and c.version.major is (component.majorVersion ? 0) c.original is component.original and c.version.major is (component.majorVersion ? 0)
componentClass = @loadClassFromCode componentModel.js, componentModel.name, 'component' componentClass = @loadClassFromCode componentModel.js, componentModel.name, 'component'
components.push [componentClass, component.config] components.push [componentClass, component.config]
if equipBy and component.original is ITEM_ORIGINAL if component.original is ITEM_ORIGINAL
component.config.ownerID = equipBy isItem = true
component.config.ownerID = equipBy if equipBy
else if component.original is EXISTS_ORIGINAL
existsConfigIndex = componentIndex
if isItem and existsConfigIndex?
# For memory usage performance, make sure these don't get any tracked properties assigned.
components[existsConfigIndex][1] = {exists: false, stateless: true}
thangTypeOriginal = thangConfig.thangType thangTypeOriginal = thangConfig.thangType
thangTypeModel = _.find thangTypes, (t) -> t.original is thangTypeOriginal thangTypeModel = _.find thangTypes, (t) -> t.original is thangTypeOriginal
return console.error thangConfig.id ? equipBy, 'could not find ThangType for', thangTypeOriginal unless thangTypeModel return console.error thangConfig.id ? equipBy, 'could not find ThangType for', thangTypeOriginal unless thangTypeModel
@ -347,6 +354,7 @@ module.exports = class World
serialize: -> serialize: ->
# Code hotspot; optimize it # Code hotspot; optimize it
@freeMemoryBeforeFinalSerialization() if @ended
startFrame = @framesSerializedSoFar startFrame = @framesSerializedSoFar
endFrame = @frames.length endFrame = @frames.length
#console.log "... world serializing frames from", startFrame, "to", endFrame, "of", @totalFrames #console.log "... world serializing frames from", startFrame, "to", endFrame, "of", @totalFrames
@ -437,6 +445,7 @@ module.exports = class World
o.scriptNotes = (sn.serialize() for sn in @scriptNotes) o.scriptNotes = (sn.serialize() for sn in @scriptNotes)
if o.scriptNotes.length > 200 if o.scriptNotes.length > 200
console.log 'Whoa, serializing a lot of WorldScriptNotes here:', o.scriptNotes.length console.log 'Whoa, serializing a lot of WorldScriptNotes here:', o.scriptNotes.length
@freeMemoryAfterEachSerialization() unless @ended
{serializedWorld: o, transferableObjects: [o.storageBuffer], startFrame: startFrame, endFrame: endFrame} {serializedWorld: o, transferableObjects: [o.storageBuffer], startFrame: startFrame, endFrame: endFrame}
@deserialize: (o, classMap, oldSerializedWorldFrames, finishedWorldCallback, startFrame, endFrame, level, streamingWorld) -> @deserialize: (o, classMap, oldSerializedWorldFrames, finishedWorldCallback, startFrame, endFrame, level, streamingWorld) ->
@ -535,6 +544,13 @@ module.exports = class World
console.log 'No frames were changed out of all', @frames.length console.log 'No frames were changed out of all', @frames.length
firstChangedFrame firstChangedFrame
freeMemoryBeforeFinalSerialization: ->
@levelComponents = null
@thangTypes = null
freeMemoryAfterEachSerialization: ->
@frames[i] = null for frame, i in @frames when i < @frames.length - 1
pointsForThang: (thangID, frameStart=0, frameEnd=null, camera=null, resolution=4) -> pointsForThang: (thangID, frameStart=0, frameEnd=null, camera=null, resolution=4) ->
# Optimized # Optimized
@pointsForThangCache ?= {} @pointsForThangCache ?= {}

View file

@ -17,7 +17,7 @@ module.exports = class WorldFrame
return nextFrame return nextFrame
setState: -> setState: ->
for thang in @world.thangs for thang in @world.thangs when not thang.stateless
@thangStateMap[thang.id] = thang.getState() @thangStateMap[thang.id] = thang.getState()
restoreState: -> restoreState: ->

View file

@ -28,7 +28,7 @@ module.exports = nativeDescription: "Deutsch (Deutschland)", englishDescription:
about: "Über" about: "Über"
contact: "Kontakt" contact: "Kontakt"
twitter_follow: "Twitter" twitter_follow: "Twitter"
# teachers: "Teachers" teachers: "Lehrer"
modal: modal:
close: "Schließen" close: "Schließen"
@ -51,22 +51,22 @@ module.exports = nativeDescription: "Deutsch (Deutschland)", englishDescription:
players: "Spieler" # Hover over a level on /play players: "Spieler" # Hover over a level on /play
hours_played: "Stunden gespielt" # Hover over a level on /play hours_played: "Stunden gespielt" # Hover over a level on /play
items: "Gegenstände" # Tooltip on item shop button from /play items: "Gegenstände" # Tooltip on item shop button from /play
# unlock: "Unlock" # For purchasing items and heroes unlock: "Entsperren" # For purchasing items and heroes
# confirm: "Confirm" confirm: "Bestätigen"
# owned: "Owned" # For items you own owned: "Besitzen" # For items you own
# locked: "Locked" locked: "Gesperrt"
# available: "Available" available: "Verfügbar"
# skills_granted: "Skills Granted" # Property documentation details skills_granted: "Verfügbare Fähigkeiten" # Property documentation details
heroes: "Helden" # Tooltip on hero shop button from /play heroes: "Helden" # Tooltip on hero shop button from /play
achievements: "Achievements" # Tooltip on achievement list button from /play achievements: "Erfolge" # Tooltip on achievement list button from /play
account: "Account" # Tooltip on account button from /play account: "Account" # Tooltip on account button from /play
settings: "Einstellungen" # Tooltip on settings button from /play settings: "Einstellungen" # Tooltip on settings button from /play
# next: "Next" # Go from choose hero to choose inventory before playing a level next: "Nächster" # Go from choose hero to choose inventory before playing a level
# change_hero: "Change Hero" # Go back from choose inventory to choose hero change_hero: "Held wechseln" # Go back from choose inventory to choose hero
choose_inventory: "Gegenstände ausrüsten" choose_inventory: "Gegenstände ausrüsten"
# buy_gems: "Buy Gems" # buy_gems: "Buy Gems"
# older_campaigns: "Older Campaigns" older_campaigns: "Ältere Kampagne"
# anonymous: "Anonymous Player" anonymous: "Anonymer Spieler"
level_difficulty: "Schwierigkeit: " level_difficulty: "Schwierigkeit: "
campaign_beginner: "Anfängerkampagne" campaign_beginner: "Anfängerkampagne"
# awaiting_levels_adventurer_prefix: "We release five levels per week." # awaiting_levels_adventurer_prefix: "We release five levels per week."
@ -76,7 +76,7 @@ module.exports = nativeDescription: "Deutsch (Deutschland)", englishDescription:
adventurer_prefix: "Du kannst zu jedem Level springen oder diskutiere die Level " adventurer_prefix: "Du kannst zu jedem Level springen oder diskutiere die Level "
adventurer_forum: "im Abenteurerforum" adventurer_forum: "im Abenteurerforum"
adventurer_suffix: "." adventurer_suffix: "."
# campaign_old_beginner: "Old Beginner Campaign" campaign_old_beginner: "Alte Anfänger Kampagne"
campaign_old_beginner_description: "... in der Du die Zauberei der Programmierung lernst." campaign_old_beginner_description: "... in der Du die Zauberei der Programmierung lernst."
campaign_dev: "Beliebiges schwierigeres Level" campaign_dev: "Beliebiges schwierigeres Level"
campaign_dev_description: "... in welchem Du die Bedienung erlernst, indem Du etwas schwierigeres machst." campaign_dev_description: "... in welchem Du die Bedienung erlernst, indem Du etwas schwierigeres machst."
@ -86,8 +86,8 @@ module.exports = nativeDescription: "Deutsch (Deutschland)", englishDescription:
campaign_player_created_description: "... in welchem Du gegen die Kreativität eines <a href=\"/contribute#artisan\">Artisan Zauberers</a> kämpfst." campaign_player_created_description: "... in welchem Du gegen die Kreativität eines <a href=\"/contribute#artisan\">Artisan Zauberers</a> kämpfst."
campaign_classic_algorithms: "Klassiche Algorithmen" campaign_classic_algorithms: "Klassiche Algorithmen"
campaign_classic_algorithms_description: "... in welchem du die populärsten Algorithmen der Informatik lernst." campaign_classic_algorithms_description: "... in welchem du die populärsten Algorithmen der Informatik lernst."
# campaign_forest: "Forest Campaign" campaign_forest: "Forest Kampagne"
# campaign_dungeon: "Dungeon Campaign" campaign_dungeon: "Dungeon Kampagne"
login: login:
sign_up: "Registrieren" sign_up: "Registrieren"
@ -118,12 +118,12 @@ module.exports = nativeDescription: "Deutsch (Deutschland)", englishDescription:
recovery_sent: "Wiederherstellungs-Email versandt." recovery_sent: "Wiederherstellungs-Email versandt."
items: items:
# primary: "Primary" primary: "Primär"
# secondary: "Secondary" secondary: "Sekundär"
armor: "Rüstung" armor: "Rüstung"
accessories: "Zubehör" accessories: "Zubehör"
misc: "Sonstiges" misc: "Sonstiges"
# books: "Books" books: "Bücher"
common: common:
loading: "Lade..." loading: "Lade..."
@ -138,8 +138,8 @@ module.exports = nativeDescription: "Deutsch (Deutschland)", englishDescription:
fork: "Fork" fork: "Fork"
play: "Abspielen" # When used as an action verb, like "Play next level" play: "Abspielen" # When used as an action verb, like "Play next level"
retry: "Erneut versuchen" retry: "Erneut versuchen"
# watch: "Watch" watch: "Verfolgen"
# unwatch: "Unwatch" unwatch: "Nicht verfolgen"
submit_patch: "Patch einreichen" submit_patch: "Patch einreichen"
general: general:
@ -193,21 +193,21 @@ module.exports = nativeDescription: "Deutsch (Deutschland)", englishDescription:
play_level: play_level:
done: "Fertig" done: "Fertig"
home: "Startseite" # Not used any more, will be removed soon. home: "Startseite" # Not used any more, will be removed soon.
# level: "Level" # Like "Level: Dungeons of Kithgard" level: "Level" # Like "Level: Dungeons of Kithgard"
# skip: "Skip" skip: "Überspringen"
game_menu: "Spielmenü" game_menu: "Spielmenü"
guide: "Hilfe" guide: "Hilfe"
restart: "Neustart" restart: "Neustart"
goals: "Ziele" goals: "Ziele"
# goal: "Goal" goal: "Ziel"
# running: "Running..." running: "Läuft..."
success: "Erfolgreich!" success: "Erfolgreich!"
incomplete: "Unvollständig" incomplete: "Unvollständig"
timed_out: "Zeit abgelaufen" timed_out: "Zeit abgelaufen"
# failing: "Failing" failing: "Fehlgeschlagen"
action_timeline: "Aktionszeitstrahl" action_timeline: "Aktionszeitstrahl"
click_to_select: "Klicke auf eine Einheit, um sie auszuwählen." click_to_select: "Klicke auf eine Einheit, um sie auszuwählen."
# reload: "Reload" reload: "Neu laden"
reload_title: "Gesamten Code neu laden?" reload_title: "Gesamten Code neu laden?"
reload_really: "Bist Du sicher, dass Du das Level neu beginnen willst?" reload_really: "Bist Du sicher, dass Du das Level neu beginnen willst?"
reload_confirm: "Alles neu laden" reload_confirm: "Alles neu laden"
@ -217,14 +217,14 @@ module.exports = nativeDescription: "Deutsch (Deutschland)", englishDescription:
victory_sign_up_poke: "Möchtest Du Neuigkeiten per Mail erhalten? Erstelle einen kostenlosen Account und wir halten Dich auf dem Laufenden." victory_sign_up_poke: "Möchtest Du Neuigkeiten per Mail erhalten? Erstelle einen kostenlosen Account und wir halten Dich auf dem Laufenden."
victory_rate_the_level: "Bewerte das Level: " # Only in old-style levels. victory_rate_the_level: "Bewerte das Level: " # Only in old-style levels.
victory_return_to_ladder: "Zurück zur Rangliste" victory_return_to_ladder: "Zurück zur Rangliste"
# victory_play_continue: "Continue" victory_play_continue: "Fortsetzen"
# victory_play_skip: "Skip Ahead" victory_play_skip: "Überspringen"
victory_play_next_level: "Spiel das nächste Level" victory_play_next_level: "Spiel das nächste Level"
# victory_play_more_practice: "More Practice" victory_play_more_practice: "Mehr Training"
# victory_play_too_easy: "Too Easy" victory_play_too_easy: "Zu einfach"
# victory_play_just_right: "Just Right" victory_play_just_right: "Genau richtig"
# victory_play_too_hard: "Too Hard" victory_play_too_hard: "Zu schwer"
# victory_saving_progress: "Saving Progress" victory_saving_progress: "Fortschritt speichern"
victory_go_home: "Geh auf die Startseite" # Only in old-style levels. victory_go_home: "Geh auf die Startseite" # Only in old-style levels.
victory_review: "Erzähl uns davon!" # Only in old-style levels. victory_review: "Erzähl uns davon!" # Only in old-style levels.
victory_hour_of_code_done: "Bist Du fertig?" victory_hour_of_code_done: "Bist Du fertig?"
@ -233,24 +233,24 @@ module.exports = nativeDescription: "Deutsch (Deutschland)", englishDescription:
tome_minion_spells: "Die Zaubersprüche Deiner Knechte" # Only in old-style levels. tome_minion_spells: "Die Zaubersprüche Deiner Knechte" # Only in old-style levels.
tome_read_only_spells: "Nur-lesen Zauberspüche" # Only in old-style levels. tome_read_only_spells: "Nur-lesen Zauberspüche" # Only in old-style levels.
tome_other_units: "Andere Einheiten" # Only in old-style levels. tome_other_units: "Andere Einheiten" # Only in old-style levels.
# tome_cast_button_run: "Run" tome_cast_button_run: "Run"
# tome_cast_button_running: "Running" tome_cast_button_running: "Running"
# tome_cast_button_ran: "Ran" tome_cast_button_ran: "Ran"
# tome_submit_button: "Submit" tome_submit_button: "Senden"
# tome_reload_method: "Reload original code for this method" # Title text for individual method reload button. tome_reload_method: "Original Code für diese Methode neu laden" # Title text for individual method reload button.
# tome_select_method: "Select a Method" tome_select_method: "Methode auswählen"
# tome_see_all_methods: "See all methods you can edit" # Title text for method list selector (shown when there are multiple programmable methdos). tome_see_all_methods: "Alle bearbeitbare Methoden anzeigen" # Title text for method list selector (shown when there are multiple programmable methdos).
tome_select_a_thang: "Wähle jemanden aus, um " tome_select_a_thang: "Wähle jemanden aus, um "
tome_available_spells: "Verfügbare Zauber" tome_available_spells: "Verfügbare Zauber"
# tome_your_skills: "Your Skills" tome_your_skills: "Deine Fähigkeiten"
# tome_current_method: "Current Method" tome_current_method: "Aktuelle Methode"
# hud_continue_short: "Continue" hud_continue_short: "Fortsetzen"
# code_saved: "Code Saved" code_saved: "Code gespeichert"
skip_tutorial: "Überspringen (Esc)" skip_tutorial: "Überspringen (Esc)"
keyboard_shortcuts: "Tastenkürzel" keyboard_shortcuts: "Tastenkürzel"
loading_ready: "Bereit!" loading_ready: "Bereit!"
# loading_start: "Start Level" loading_start: "Starte Level"
# problem_alert_title: "Fix Your Code" problem_alert_title: "Repariere deinen Code"
time_current: "Aktuell" time_current: "Aktuell"
time_total: "Total" time_total: "Total"
time_goto: "Gehe zu" time_goto: "Gehe zu"
@ -270,7 +270,7 @@ module.exports = nativeDescription: "Deutsch (Deutschland)", englishDescription:
tip_baby_coders: "In der Zukunft werden sogar Babies Erzmagier sein." tip_baby_coders: "In der Zukunft werden sogar Babies Erzmagier sein."
tip_morale_improves: "Das Laden wird weiter gehen bis die Stimmung sich verbessert." tip_morale_improves: "Das Laden wird weiter gehen bis die Stimmung sich verbessert."
tip_all_species: "Wir glauben an gleiche Chancen für alle Arten Programmieren zu lernen." tip_all_species: "Wir glauben an gleiche Chancen für alle Arten Programmieren zu lernen."
# tip_reticulating: "Reticulating spines." tip_reticulating: "Spines neuberechnen."
tip_harry: "Du bist ein Zauberer, " tip_harry: "Du bist ein Zauberer, "
tip_great_responsibility: "Mit großen Programmierfähigkeiten kommt große Verantwortung." tip_great_responsibility: "Mit großen Programmierfähigkeiten kommt große Verantwortung."
tip_munchkin: "Wenn du dein Gemüse nicht isst, besucht dich ein Zwerg während du schläfst." tip_munchkin: "Wenn du dein Gemüse nicht isst, besucht dich ein Zwerg während du schläfst."
@ -283,9 +283,9 @@ module.exports = nativeDescription: "Deutsch (Deutschland)", englishDescription:
tip_talk_is_cheap: "Reden ist billig. Zeig mir den Code. - Linus Torvalds" tip_talk_is_cheap: "Reden ist billig. Zeig mir den Code. - Linus Torvalds"
tip_first_language: "Das schwierigste, das du jemals lernen wirst, ist die erste Programmiersprache. - Alan Kay" tip_first_language: "Das schwierigste, das du jemals lernen wirst, ist die erste Programmiersprache. - Alan Kay"
tip_hardware_problem: "Q: Wie viele Programmierer braucht man um eine Glühbirne auszuwechseln? A: Keine, es ist ein Hardware-Problem." tip_hardware_problem: "Q: Wie viele Programmierer braucht man um eine Glühbirne auszuwechseln? A: Keine, es ist ein Hardware-Problem."
# tip_hofstadters_law: "Hofstadter's Law: It always takes longer than you expect, even when you take into account Hofstadter's Law." tip_hofstadters_law: "Hofstadter's Gesetz: Es dauert immer länger als erwartet, auch wenn du Hofstadter's Gesetz anwendest."
# tip_premature_optimization: "Premature optimization is the root of all evil. - Donald Knuth" tip_premature_optimization: "Vorzeitige Optimierung ist die Wurzel alles Übels (oder der mindestens Meister) bei der Programmierung - Donald Knuth"
# tip_brute_force: "When in doubt, use brute force. - Ken Thompson" tip_brute_force: "Verwende im Zweifelsfall rohe Gewalt. - Ken Thompson"
customize_wizard: "Bearbeite den Zauberer" customize_wizard: "Bearbeite den Zauberer"
game_menu: game_menu:
@ -294,24 +294,24 @@ module.exports = nativeDescription: "Deutsch (Deutschland)", englishDescription:
options_tab: "Einstellungen" options_tab: "Einstellungen"
guide_tab: "Guide" guide_tab: "Guide"
multiplayer_tab: "Mehrspieler" multiplayer_tab: "Mehrspieler"
# auth_tab: "Sign Up" auth_tab: "Registrieren"
inventory_caption: "Rüste deinen Helden aus" inventory_caption: "Rüste deinen Helden aus"
choose_hero_caption: "Wähle Helden, Sprache" choose_hero_caption: "Wähle Helden, Sprache"
save_load_caption: "... und schaue dir die Historie an" save_load_caption: "... und schaue dir die Historie an"
options_caption: "konfiguriere Einstellungen" options_caption: "konfiguriere Einstellungen"
guide_caption: "Doku und Tipps" guide_caption: "Doku und Tipps"
multiplayer_caption: "Spiele mit Freunden!" multiplayer_caption: "Spiele mit Freunden!"
# auth_caption: "Save your progress." auth_caption: "Fortschritt speichern."
inventory: inventory:
choose_inventory: "Gegenstände ausrüsten" choose_inventory: "Gegenstände ausrüsten"
# equipped_item: "Equipped" equipped_item: "Hinzugefügt"
# available_item: "Available" available_item: "Verfügbar"
# restricted_title: "Restricted" # restricted_title: "Restricted"
# should_equip: "(double-click to equip)" should_equip: "(Doppelklick zum Hinzufügen)"
# equipped: "(equipped)" equipped: "(hinzugefügt)"
# locked: "(locked)" locked: "(gesperrt)"
# restricted: "(restricted in this level)" restricted: "(benötigt für dieses Level)"
# equip: "Equip" # equip: "Equip"
# unequip: "Unequip" # unequip: "Unequip"
@ -334,16 +334,16 @@ module.exports = nativeDescription: "Deutsch (Deutschland)", englishDescription:
io_blurb: "Simpel aber obskur." io_blurb: "Simpel aber obskur."
status: "Status" status: "Status"
weapons: "Waffen" weapons: "Waffen"
# weapons_warrior: "Swords - Short Range, No Magic" weapons_warrior: "Schwert - Kurze Reichweite, Kein Zauber"
# weapons_ranger: "Crossbows, Guns - Long Range, No Magic" weapons_ranger: "Armbrust, Geschütz - Hohe Reichweite, Kein Zauber"
# weapons_wizard: "Wands, Staffs - Long Range, Magic" weapons_wizard: "Stäbe, Stäbe - Lange Reichweite, Zauber"
# attack: "Damage" # Can also translate as "Attack" attack: "Schaden" # Can also translate as "Attack"
health: "Gesundheit" health: "Gesundheit"
speed: "Geschwindigkeit" speed: "Geschwindigkeit"
# regeneration: "Regeneration" regeneration: "Regeneration"
# range: "Range" # As in "attack or visual range" range: "Reichweite" # As in "attack or visual range"
# blocks: "Blocks" # As in "this shield blocks this much damage" blocks: "Blockieren" # As in "this shield blocks this much damage"
# skills: "Skills" skills: "Fähigkeiten"
save_load: save_load:
granularity_saved_games: "Gespeichert" granularity_saved_games: "Gespeichert"
@ -584,10 +584,10 @@ module.exports = nativeDescription: "Deutsch (Deutschland)", englishDescription:
edit_article_title: "Artikel bearbeiten" edit_article_title: "Artikel bearbeiten"
contribute: contribute:
# page_title: "Contributing" page_title: "Mitwirken"
character_classes_title: "Charakter Klassen" character_classes_title: "Charakter Klassen"
introduction_desc_intro: "Wir haben hohe Erwartungen für CodeCombat." introduction_desc_intro: "Wir haben hohe Erwartungen für CodeCombat."
# introduction_desc_pref: "We want to be where programmers of all stripes come to learn and play together, introduce others to the wonderful world of coding, and reflect the best parts of the community. We can't and don't want to do that alone; what makes projects like GitHub, Stack Overflow and Linux great are the people who use them and build on them. To that end, " # introduction_desc_pref: "We want to be where programmers of all stripes come to learn and play together, introduce others to the wonderful world of coding, and reflect the best parts of the community. We can't and don't want to do that alone; what makes projects like GitHub, Stack Overflow and Linux great are the people who use them and build on them. To that end, "
introduction_desc_github_url: "CodeCombat ist komplett OpenSource" introduction_desc_github_url: "CodeCombat ist komplett OpenSource"
# introduction_desc_suf: ", and we aim to provide as many ways as possible for you to take part and make this project as much yours as ours." # introduction_desc_suf: ", and we aim to provide as many ways as possible for you to take part and make this project as much yours as ours."
introduction_desc_ending: "Wir hoffen du nimmst an unserer Party teil!" introduction_desc_ending: "Wir hoffen du nimmst an unserer Party teil!"
@ -597,7 +597,7 @@ module.exports = nativeDescription: "Deutsch (Deutschland)", englishDescription:
# archmage_summary: "Interested in working on game graphics, user interface design, database and server organization, multiplayer networking, physics, sound, or game engine performance? Want to help build a game to help other people learn what you are good at? We have a lot to do and if you are an experienced programmer and want to develop for CodeCombat, this class is for you. We would love your help building the best programming game ever." # archmage_summary: "Interested in working on game graphics, user interface design, database and server organization, multiplayer networking, physics, sound, or game engine performance? Want to help build a game to help other people learn what you are good at? We have a lot to do and if you are an experienced programmer and want to develop for CodeCombat, this class is for you. We would love your help building the best programming game ever."
# archmage_introduction: "One of the best parts about building games is they synthesize so many different things. Graphics, sound, real-time networking, social networking, and of course many of the more common aspects of programming, from low-level database management, and server administration to user facing design and interface building. There's a lot to do, and if you're an experienced programmer with a hankering to really dive into the nitty-gritty of CodeCombat, this class might be for you. We would love to have your help building the best programming game ever." # archmage_introduction: "One of the best parts about building games is they synthesize so many different things. Graphics, sound, real-time networking, social networking, and of course many of the more common aspects of programming, from low-level database management, and server administration to user facing design and interface building. There's a lot to do, and if you're an experienced programmer with a hankering to really dive into the nitty-gritty of CodeCombat, this class might be for you. We would love to have your help building the best programming game ever."
class_attributes: "Klassenattribute" class_attributes: "Klassenattribute"
# archmage_attribute_1_pref: "Knowledge in " archmage_attribute_1_pref: "Kentnisse in "
# archmage_attribute_1_suf: ", or a desire to learn. Most of our code is in this language. If you're a fan of Ruby or Python, you'll feel right at home. It's JavaScript, but with a nicer syntax." # archmage_attribute_1_suf: ", or a desire to learn. Most of our code is in this language. If you're a fan of Ruby or Python, you'll feel right at home. It's JavaScript, but with a nicer syntax."
# archmage_attribute_2: "Some experience in programming and personal initiative. We'll help you get oriented, but we can't spend much time training you." # archmage_attribute_2: "Some experience in programming and personal initiative. We'll help you get oriented, but we can't spend much time training you."
# how_to_join: "How To Join" # how_to_join: "How To Join"

View file

@ -319,6 +319,9 @@
few_gems: 'A few gems' few_gems: 'A few gems'
pile_gems: 'Pile of gems' pile_gems: 'Pile of gems'
chest_gems: 'Chest of gems' chest_gems: 'Chest of gems'
purchasing: 'Purchasing...'
declined: 'Your card was declined'
retrying: 'Server error, retrying.'
choose_hero: choose_hero:
choose_hero: "Choose Your Hero" choose_hero: "Choose Your Hero"

View file

@ -28,7 +28,10 @@ module.exports = class SuperModel extends Backbone.Model
unfinished unfinished
loadModel: (model, name, fetchOptions, value=1) -> loadModel: (model, name, fetchOptions, value=1) ->
cachedModel = @getModelByURL(model.getURL()) # hero-ladder levels need remote opponent_session for latest session data (e.g. code)
# Can't apply to everything since other features rely on cached models being more recent (E.g. level_session)
# E.g.#2 heroConfig isn't necessarily saved to db in world map inventory modal, so we need to load the cached session on level start
cachedModel = @getModelByURL(model.getURL()) unless fetchOptions?.cache is false and name is 'opponent_session'
if cachedModel if cachedModel
if cachedModel.loaded if cachedModel.loaded
res = @addModelResource(cachedModel, name, fetchOptions, 0) res = @addModelResource(cachedModel, name, fetchOptions, 0)

View file

@ -8,6 +8,7 @@ PaymentSchema = c.object({title: 'Payment', required: []}, {
amount: { type: 'integer', description: 'Payment in cents.' } amount: { type: 'integer', description: 'Payment in cents.' }
created: c.date({title: 'Created', readOnly: true}) created: c.date({title: 'Created', readOnly: true})
gems: { type: 'integer', description: 'The number of gems acquired.' } gems: { type: 'integer', description: 'The number of gems acquired.' }
productID: { enum: ['gems_5', 'gems_10', 'gems_20']}
ios: c.object({title: 'iOS IAP Data'}, { ios: c.object({title: 'iOS IAP Data'}, {
transactionID: { type: 'string' } transactionID: { type: 'string' }
@ -17,7 +18,7 @@ PaymentSchema = c.object({title: 'Payment', required: []}, {
stripe: c.object({title: 'Stripe Data'}, { stripe: c.object({title: 'Stripe Data'}, {
timestamp: { type: 'integer', description: 'Unique identifier provided by the client, to guard against duplicate payments.' } timestamp: { type: 'integer', description: 'Unique identifier provided by the client, to guard against duplicate payments.' }
transactionID: { type: 'string' } chargeID: { type: 'string' }
customerID: { type: 'string' } customerID: { type: 'string' }
}) })
}) })

View file

@ -270,6 +270,7 @@ _.extend UserSchema.properties,
earned: c.RewardSchema 'earned by achievements' earned: c.RewardSchema 'earned by achievements'
purchased: c.RewardSchema 'purchased with gems or money' purchased: c.RewardSchema 'purchased with gems or money'
spent: {type: 'number'} spent: {type: 'number'}
stripeCustomerID: { type: 'string' }
c.extendBasicProperties UserSchema, 'user' c.extendBasicProperties UserSchema, 'user'

View file

@ -51,4 +51,9 @@ module.exports =
'buy-gems-modal:update-products': { } 'buy-gems-modal:update-products': { }
'buy-gems-modal:purchase-initiated': c.object {required: ['productID']}, 'buy-gems-modal:purchase-initiated': c.object {required: ['productID']},
productID: { type: 'string' } productID: { type: 'string' }
'stripe:received-token': c.object { required: ['token'] },
token: { type: 'object', properties: {
id: {type: 'string'}
}}

View file

@ -1,15 +1,14 @@
c = require 'schemas/schemas' c = require 'schemas/schemas'
module.exports = module.exports =
'real-time-multiplayer:created-game': c.object {title: 'Multiplayer created game', required: ['session']}, 'real-time-multiplayer:created-game': c.object {title: 'Multiplayer created game', required: ['realTimeSessionID']},
session: {type: 'object'} realTimeSessionID: {type: 'string'}
'real-time-multiplayer:joined-game': c.object {title: 'Multiplayer joined game', required: ['id', 'session']}, 'real-time-multiplayer:joined-game': c.object {title: 'Multiplayer joined game', required: ['realTimeSessionID']},
id: {type: 'string'} realTimeSessionID: {type: 'string'}
session: {type: 'object'}
'real-time-multiplayer:left-game': c.object {title: 'Multiplayer left game', required: ['id']}, 'real-time-multiplayer:left-game': c.object {title: 'Multiplayer left game'},
id: {type: 'string'} userID: {type: 'string'}
'real-time-multiplayer:manual-cast': c.object {title: 'Multiplayer manual cast'} 'real-time-multiplayer:manual-cast': c.object {title: 'Multiplayer manual cast'}

View file

@ -42,3 +42,12 @@
button button
width: 80% width: 80%
//- Errors
.alert
position: absolute
left: 10%
width: 80%
top: 20px
border: 5px solid gray

View file

@ -22,7 +22,7 @@ if !ladderGame
if ladderGame if ladderGame
if me.get('anonymous') 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. p(data-i18n="multiplayer.multiplayer_sign_in_leaderboard") Sign in or create an account and get your solution on the leaderboard.
else if readyToRank else if realTimeSessions && realTimeSessionsPlayers
button#create-game-button Create Game button#create-game-button Create Game
hr hr
@ -37,7 +37,7 @@ if ladderGame
span(style="margin:10px")= currentRealTimeSession.id span(style="margin:10px")= currentRealTimeSession.id
button#leave-game-button(data-item=item) Leave Game button#leave-game-button(data-item=item) Leave Game
div div
- var players = realTimeSessionPlayers[currentRealTimeSession.id] - var players = realTimeSessionsPlayers[currentRealTimeSession.id]
if players if players
span(style="margin:10px") Players: span(style="margin:10px") Players:
- for (var i=0; i < players.length; i++) { - for (var i=0; i < players.length; i++) {
@ -61,7 +61,7 @@ if ladderGame
- continue - continue
if levelID === realTimeSessions.at(i).get('levelID') && realTimeSessions.at(i).get('state') === 'creating' if levelID === realTimeSessions.at(i).get('levelID') && realTimeSessions.at(i).get('state') === 'creating'
- var id = realTimeSessions.at(i).get('id') - var id = realTimeSessions.at(i).get('id')
- var players = realTimeSessionPlayers[id] - var players = realTimeSessionsPlayers[id]
if players && players.length === 1 if players && players.length === 1
- noOpenGames = false - noOpenGames = false
- var creatorName = realTimeSessions.at(i).get('creatorName') - var creatorName = realTimeSessions.at(i).get('creatorName')

View file

@ -1,11 +1,30 @@
.modal-dialog .modal-dialog
.modal-content .modal-content
img(src="/images/pages/play/modal/buy-gems-background.png")#buy-gems-background if state === 'purchasing'
.alert.alert-info(data-i18n="buy_gems.purchasing")
#products else if state === 'retrying'
for product in products #retrying-alert.alert.alert-danger(data-i18n="buy_gems.retrying")
.product
h4 x#{product.gems} else
h3(data-i18n=product.i18n) img(src="/images/pages/play/modal/buy-gems-background.png")#buy-gems-background
button.btn.btn-illustrated.btn-lg(value=product.id)
span= product.price #products
for product in products
.product
h4 x#{product.gems}
h3(data-i18n=product.i18n)
button.btn.btn-illustrated.btn-lg(value=product.id)
span= product.price
if state === 'declined'
#declined-alert.alert.alert-danger.alert-dismissible
span(data-i18n="buy_gems.declined")
button.close(type="button" data-dismiss="alert")
span(aria-hidden="true") &times;
if state === 'unknown_error'
#error-alert.alert.alert-danger.alert-dismissible
span(data-i18n="loading_error.unknown")
button.close(type="button" data-dismiss="alert")
span(aria-hidden="true") &times;

View file

@ -43,13 +43,13 @@
.game-controls.header-font .game-controls.header-font
button.btn.items(data-toggle='coco-modal', data-target='play/modal/PlayItemsModal', data-i18n="[title]play.items") button.btn.items(data-toggle='coco-modal', data-target='play/modal/PlayItemsModal', data-i18n="[title]play.items")
button.btn.heroes(data-toggle='coco-modal', data-target='play/modal/PlayHeroesModal', data-i18n="[title]play.heroes") button.btn.heroes(data-toggle='coco-modal', data-target='play/modal/PlayHeroesModal', data-i18n="[title]play.heroes")
if me.isAdmin() || isIPadApp if me.get('anonymous') === false
button.btn.gems(data-toggle='coco-modal', data-target='play/modal/BuyGemsModal', data-i18n="[title]play.buy_gems") button.btn.gems(data-toggle='coco-modal', data-target='play/modal/BuyGemsModal', data-i18n="[title]play.buy_gems")
if me.isAdmin() if me.isAdmin()
button.btn.achievements(data-toggle='coco-modal', data-target='play/modal/PlayAchievementsModal', data-i18n="[title]play.achievements") button.btn.achievements(data-toggle='coco-modal', data-target='play/modal/PlayAchievementsModal', data-i18n="[title]play.achievements")
button.btn.account(data-toggle='coco-modal', data-target='play/modal/PlayAccountModal', data-i18n="[title]play.account") button.btn.account(data-toggle='coco-modal', data-target='play/modal/PlayAccountModal', data-i18n="[title]play.account")
button.btn.settings(data-toggle='coco-modal', data-target='play/modal/PlaySettingsModal', data-i18n="[title]play.settings") button.btn.settings(data-toggle='coco-modal', data-target='play/modal/PlaySettingsModal', data-i18n="[title]play.settings")
else if me.get('anonymous') else if me.get('anonymous', true)
button.btn.settings(data-toggle='coco-modal', data-target='modal/AuthModal', data-i18n="[title]play.settings") button.btn.settings(data-toggle='coco-modal', data-target='modal/AuthModal', data-i18n="[title]play.settings")
// Don't show these things, they are bad and take us out of the game. Just wait until the new ones work. // Don't show these things, they are bad and take us out of the game. Just wait until the new ones work.
//else //else

View file

@ -26,7 +26,7 @@ module.exports = class MultiplayerView extends CocoView
@level = options.level @level = options.level
@session = options.session @session = options.session
@listenTo @session, 'change:multiplayer', @updateLinkSection @listenTo @session, 'change:multiplayer', @updateLinkSection
@watchRealTimeSessions() @watchRealTimeSessions() if @level?.get('type') in ['hero-ladder']
destroy: -> destroy: ->
@realTimeSessions?.off 'add', @onRealTimeSessionAdded @realTimeSessions?.off 'add', @onRealTimeSessionAdded
@ -46,15 +46,16 @@ module.exports = class MultiplayerView extends CocoView
c.readyToRank = @session?.readyToRank() c.readyToRank = @session?.readyToRank()
# Real-time multiplayer stuff # Real-time multiplayer stuff
c.levelID = @session.get('levelID') if @level?.get('type') in ['hero-ladder']
c.realTimeSessions = @realTimeSessions c.levelID = @session.get('levelID')
c.currentRealTimeSession = @currentRealTimeSession if @currentRealTimeSession c.realTimeSessions = @realTimeSessions
c.realTimeSessionPlayers = @realTimeSessionsPlayers if @realTimeSessionsPlayers c.currentRealTimeSession = @currentRealTimeSession if @currentRealTimeSession
# console.log 'MultiplayerView getRenderData', c.levelID c.realTimeSessionsPlayers = @realTimeSessionsPlayers if @realTimeSessionsPlayers
# console.log 'realTimeSessions', c.realTimeSessions # console.log 'MultiplayerView getRenderData', c.levelID
# console.log c.realTimeSessions.at(c.realTimeSessions.length - 1).get('state') if c.realTimeSessions.length > 0 # console.log 'realTimeSessions', c.realTimeSessions
# console.log 'currentRealTimeSession', c.currentRealTimeSession # console.log c.realTimeSessions.at(c.realTimeSessions.length - 1).get('state') if c.realTimeSessions.length > 0
# console.log 'realTimeSessionPlayers', c.realTimeSessionPlayers # console.log 'currentRealTimeSession', c.currentRealTimeSession
# console.log 'realTimeSessionPlayers', c.realTimeSessionsPlayers
c c
@ -134,7 +135,9 @@ module.exports = class MultiplayerView extends CocoView
# console.log 'MultiplayerView found current real-time session', rts # console.log 'MultiplayerView found current real-time session', rts
@currentRealTimeSession = new RealTimeModel('multiplayer_level_sessions/' + rts.id) @currentRealTimeSession = new RealTimeModel('multiplayer_level_sessions/' + rts.id)
@currentRealTimeSession.on 'change', @onCurrentRealTimeSessionChanged @currentRealTimeSession.on 'change', @onCurrentRealTimeSessionChanged
Backbone.Mediator.publish 'real-time-multiplayer:joined-game', id: me.id, session: @currentRealTimeSession
# TODO: Is this necessary? Shouldn't everyone already know we joined a game at this point?
Backbone.Mediator.publish 'real-time-multiplayer:joined-game', realTimeSessionID: @currentRealTimeSession.id
onRealTimeSessionAdded: (rts) => onRealTimeSessionAdded: (rts) =>
@watchRealTimeSession rts @watchRealTimeSession rts
@ -168,8 +171,13 @@ module.exports = class MultiplayerView extends CocoView
@currentRealTimeSession.on 'change', @onCurrentRealTimeSessionChanged @currentRealTimeSession.on 'change', @onCurrentRealTimeSessionChanged
# TODO: s.id === @currentRealTimeSession.id ? # TODO: s.id === @currentRealTimeSession.id ?
players = new RealTimeCollection('multiplayer_level_sessions/' + @currentRealTimeSession.id + '/players') players = new RealTimeCollection('multiplayer_level_sessions/' + @currentRealTimeSession.id + '/players')
players.create id: me.id, state: 'coding', name: @session.get('creatorName'), team: @session.get('team') players.create
Backbone.Mediator.publish 'real-time-multiplayer:created-game', session: @currentRealTimeSession id: me.id
state: 'coding'
name: @session.get('creatorName')
team: @session.get('team')
level_session: @session.id
Backbone.Mediator.publish 'real-time-multiplayer:created-game', realTimeSessionID: @currentRealTimeSession.id
@render() @render()
onJoinRealTimeGame: (e) -> onJoinRealTimeGame: (e) ->
@ -178,17 +186,31 @@ module.exports = class MultiplayerView extends CocoView
@currentRealTimeSession = @realTimeSessions.get(item.id) @currentRealTimeSession = @realTimeSessions.get(item.id)
@currentRealTimeSession.on 'change', @onCurrentRealTimeSessionChanged @currentRealTimeSession.on 'change', @onCurrentRealTimeSessionChanged
if @realTimeSessionsPlayers[item.id] if @realTimeSessionsPlayers[item.id]
@realTimeSessionsPlayers[item.id].create id: me.id, state: 'coding', name: @session.get('creatorName'), team: @session.get('team')
# TODO: SpellView updateTeam() should take care of this team swap update in the real-time multiplayer session
creatorID = @currentRealTimeSession.get('creator')
creator = @realTimeSessionsPlayers[item.id].get(creatorID)
creatorTeam = creator.get('team')
myTeam = @session.get('team')
if myTeam is creatorTeam
myTeam = if creatorTeam is 'humans' then 'ogres' else 'humans'
@realTimeSessionsPlayers[item.id].create
id: me.id
state: 'coding'
name: me.get('name')
team: myTeam
level_session: @session.id
else else
console.error 'MultiplayerView onJoinRealTimeGame did not have a players collection', @currentRealTimeSession console.error 'MultiplayerView onJoinRealTimeGame did not have a players collection', @currentRealTimeSession
Backbone.Mediator.publish 'real-time-multiplayer:joined-game', id: me.id, session: @currentRealTimeSession Backbone.Mediator.publish 'real-time-multiplayer:joined-game', realTimeSessionID: @currentRealTimeSession.id
@render() @render()
onLeaveRealTimeGame: (e) -> onLeaveRealTimeGame: (e) ->
if @currentRealTimeSession if @currentRealTimeSession
@currentRealTimeSession.off 'change', @onCurrentRealTimeSessionChanged @currentRealTimeSession.off 'change', @onCurrentRealTimeSessionChanged
@currentRealTimeSession = null @currentRealTimeSession = null
Backbone.Mediator.publish 'real-time-multiplayer:left-game', id: me.id Backbone.Mediator.publish 'real-time-multiplayer:left-game', userID: me.id
else else
console.error "Tried to leave a game with no currentMultiplayerSession" console.error "Tried to leave a game with no currentMultiplayerSession"
@render() @render()

View file

@ -84,13 +84,14 @@ module.exports = class LevelFlagsView extends CocoView
@world = @options.world = event.world @world = @options.world = event.world
onJoinedMultiplayerGame: (e) -> onJoinedMultiplayerGame: (e) ->
@realTimeFlags = new RealTimeCollection('multiplayer_level_sessions/' + e.session.id + '/flagHistory') @realTimeFlags = new RealTimeCollection('multiplayer_level_sessions/' + e.realTimeSessionID + '/flagHistory')
@realTimeFlags.on 'add', @onRealTimeMultiplayerFlagAdded @realTimeFlags.on 'add', @onRealTimeMultiplayerFlagAdded
@realTimeFlags.on 'remove', @onRealTimeMultiplayerFlagRemoved @realTimeFlags.on 'remove', @onRealTimeMultiplayerFlagRemoved
onLeftMultiplayerGame: (e) -> onLeftMultiplayerGame: (e) ->
if @realTimeFlags if @realTimeFlags
@realTimeFlags.off() @realTimeFlags.off 'add', @onRealTimeMultiplayerFlagAdded
@realTimeFlags.off 'remove', @onRealTimeMultiplayerFlagRemoved
@realTimeFlags = null @realTimeFlags = null
onRealTimeMultiplayerFlagAdded: (e) => onRealTimeMultiplayerFlagAdded: (e) =>

View file

@ -98,6 +98,9 @@ module.exports = class PlayLevelView extends RootView
@isEditorPreview = @getQueryVariable 'dev' @isEditorPreview = @getQueryVariable 'dev'
@sessionID = @getQueryVariable 'session' @sessionID = @getQueryVariable 'session'
@opponentSessionID = @getQueryVariable('opponent')
@opponentSessionID ?= @options.opponent
$(window).on 'resize', @onWindowResize $(window).on 'resize', @onWindowResize
@saveScreenshot = _.throttle @saveScreenshot, 30000 @saveScreenshot = _.throttle @saveScreenshot, 30000
@ -131,7 +134,7 @@ module.exports = class PlayLevelView extends RootView
load: -> load: ->
@loadStartTime = new Date() @loadStartTime = new Date()
@god = new God debugWorker: true @god = new God debugWorker: true
@levelLoader = new LevelLoader supermodel: @supermodel, levelID: @levelID, sessionID: @sessionID, opponentSessionID: @getQueryVariable('opponent'), team: @getQueryVariable('team') @levelLoader = new LevelLoader supermodel: @supermodel, levelID: @levelID, sessionID: @sessionID, opponentSessionID: @opponentSessionID, team: @getQueryVariable('team')
@listenToOnce @levelLoader, 'world-necessities-loaded', @onWorldNecessitiesLoaded @listenToOnce @levelLoader, 'world-necessities-loaded', @onWorldNecessitiesLoaded
trackLevelLoadEnd: -> trackLevelLoadEnd: ->
@ -170,7 +173,7 @@ module.exports = class PlayLevelView extends RootView
onWorldNecessitiesLoaded: -> onWorldNecessitiesLoaded: ->
# Called when we have enough to build the world, but not everything is loaded # Called when we have enough to build the world, but not everything is loaded
@grabLevelLoaderData() @grabLevelLoaderData()
team = @getQueryVariable('team') ? @world.teamForPlayer(0) team = @getQueryVariable('team') ? @session.get('team') ? @world.teamForPlayer(0)
@loadOpponentTeam(team) @loadOpponentTeam(team)
@setupGod() @setupGod()
@setTeam team @setTeam team
@ -190,6 +193,8 @@ module.exports = class PlayLevelView extends RootView
@level = @levelLoader.level @level = @levelLoader.level
@$el.addClass 'hero' if @level.get('type', true) in ['hero', 'hero-ladder', 'hero-coop'] @$el.addClass 'hero' if @level.get('type', true) in ['hero', 'hero-ladder', 'hero-coop']
@$el.addClass 'flags' if _.any(@world.thangs, (t) -> (t.programmableProperties and 'findFlags' in t.programmableProperties) or t.inventory?.flag) or @level.get('slug') is 'sky-span' @$el.addClass 'flags' if _.any(@world.thangs, (t) -> (t.programmableProperties and 'findFlags' in t.programmableProperties) or t.inventory?.flag) or @level.get('slug') is 'sky-span'
# TODO: Update terminology to always be opponentSession or otherSession
# TODO: E.g. if it's always opponent right now, then variable names should be opponentSession until we have coop play
@otherSession = @levelLoader.opponentSession @otherSession = @levelLoader.opponentSession
@worldLoadFakeResources = [] # first element (0) is 1%, last (100) is 100% @worldLoadFakeResources = [] # first element (0) is 1%, last (100) is 100%
for percent in [1 .. 100] for percent in [1 .. 100]
@ -211,7 +216,8 @@ module.exports = class PlayLevelView extends RootView
opponentSpells = opponentSpells.concat spells opponentSpells = opponentSpells.concat spells
if (not @session.get('teamSpells')) and @otherSession?.get('teamSpells') if (not @session.get('teamSpells')) and @otherSession?.get('teamSpells')
@session.set('teamSpells', @otherSession.get('teamSpells')) @session.set('teamSpells', @otherSession.get('teamSpells'))
opponentCode = @otherSession?.get('transpiledCode') or {} # hero-ladder levels use code instead of transpiledCode
opponentCode = @otherSession?.get('transpiledCode') or @otherSession?.get('code') or {}
myCode = @session.get('code') or {} myCode = @session.get('code') or {}
for spell in opponentSpells for spell in opponentSpells
[thang, spell] = spell.split '/' [thang, spell] = spell.split '/'
@ -232,6 +238,7 @@ module.exports = class PlayLevelView extends RootView
team = team?.team unless _.isString team team = team?.team unless _.isString team
team ?= 'humans' team ?= 'humans'
me.team = team me.team = team
@session.set 'team', team
Backbone.Mediator.publish 'level:team-set', team: team # Needed for scripts Backbone.Mediator.publish 'level:team-set', team: team # Needed for scripts
@team = team @team = team
@ -248,7 +255,7 @@ module.exports = class PlayLevelView extends RootView
@insertSubView new HUDView {level: @level} @insertSubView new HUDView {level: @level}
@insertSubView new LevelDialogueView {level: @level} @insertSubView new LevelDialogueView {level: @level}
@insertSubView new ChatView levelID: @levelID, sessionID: @session.id, session: @session @insertSubView new ChatView levelID: @levelID, sessionID: @session.id, session: @session
if @level.get('type') in ['ladder', 'hero-ladder'] if @level.get('type') in ['hero-ladder']
@insertSubView new MultiplayerStatusView levelID: @levelID, session: @session, level: @level @insertSubView new MultiplayerStatusView levelID: @levelID, session: @session, level: @level
@insertSubView new ProblemAlertView {} @insertSubView new ProblemAlertView {}
worldName = utils.i18n @level.attributes, 'name' worldName = utils.i18n @level.attributes, 'name'
@ -283,7 +290,7 @@ module.exports = class PlayLevelView extends RootView
@setupManager = new LevelSetupManager({supermodel: @supermodel, levelID: @levelID, parent: @, session: @session}) @setupManager = new LevelSetupManager({supermodel: @supermodel, levelID: @levelID, parent: @, session: @session})
@setupManager.open() @setupManager.open()
@onRealTimeMultiplayerLevelLoaded e.session if e.level.get('type') in ['ladder', 'hero-ladder'] @onRealTimeMultiplayerLevelLoaded() if e.level.get('type') in ['hero-ladder']
onLoaded: -> onLoaded: ->
_.defer => @onLevelLoaderLoaded() _.defer => @onLevelLoaderLoaded()
@ -342,6 +349,9 @@ module.exports = class PlayLevelView extends RootView
@removeSubView @loadingView @removeSubView @loadingView
@loadingView = null @loadingView = null
@playAmbientSound() @playAmbientSound()
if @options.realTimeMultiplayerSessionID?
Backbone.Mediator.publish 'playback:real-time-playback-waiting', {}
@realTimeMultiplayerContinueGame @options.realTimeMultiplayerSessionID
application.tracker?.trackEvent 'Play Level', Action: 'Start Level', levelID: @levelID application.tracker?.trackEvent 'Play Level', Action: 'Start Level', levelID: @levelID
playAmbientSound: -> playAmbientSound: ->
@ -359,7 +369,7 @@ module.exports = class PlayLevelView extends RootView
return if @alreadyLoadedState return if @alreadyLoadedState
@alreadyLoadedState = true @alreadyLoadedState = true
state = @originalSessionState state = @originalSessionState
if @level.get('type', true) in ['hero', 'hero-ladder', 'hero-coop'] if not @level or @level.get('type', true) in ['hero', 'hero-ladder', 'hero-coop']
Backbone.Mediator.publish 'level:suppress-selection-sounds', suppress: true Backbone.Mediator.publish 'level:suppress-selection-sounds', suppress: true
Backbone.Mediator.publish 'tome:select-primary-sprite', {} Backbone.Mediator.publish 'tome:select-primary-sprite', {}
Backbone.Mediator.publish 'level:suppress-selection-sounds', suppress: false Backbone.Mediator.publish 'level:suppress-selection-sounds', suppress: false
@ -540,7 +550,9 @@ module.exports = class PlayLevelView extends RootView
onSubmissionComplete: => onSubmissionComplete: =>
return if @destroyed return if @destroyed
Backbone.Mediator.publish 'level:show-victory', showModal: true if @goalManager.checkOverallStatus() is 'success' # TODO: Show a victory dialog specific to hero-ladder level
if @goalManager.checkOverallStatus() is 'success' and not @options.realTimeMultiplayerSessionID?
Backbone.Mediator.publish 'level:show-victory', showModal: true
destroy: -> destroy: ->
@levelLoader?.destroy() @levelLoader?.destroy()
@ -572,6 +584,8 @@ module.exports = class PlayLevelView extends RootView
# Updates real-time multiplayer player state # Updates real-time multiplayer player state
# Cleans up old sessions (sets state to 'finished') # Cleans up old sessions (sets state to 'finished')
# Real-time multiplayer cast handshake # Real-time multiplayer cast handshake
# Swap teams on game joined, if necessary
# Reload PlayLevelView on real-time submit, automatically continue game and real-time playback
# #
# It monitors these: # It monitors these:
# Real-time multiplayer sessions # Real-time multiplayer sessions
@ -585,19 +599,23 @@ module.exports = class PlayLevelView extends RootView
# @realTimeOpponent - Current real-time multiplayer opponent # @realTimeOpponent - Current real-time multiplayer opponent
# @realTimePlayers - Real-time players for current real-time multiplayer game session # @realTimePlayers - Real-time players for current real-time multiplayer game session
# @realTimeSessionCollection - Collection of all real-time multiplayer sessions # @realTimeSessionCollection - Collection of all real-time multiplayer sessions
# @options.realTimeMultiplayerSessionID - Need to continue an existing real-time multiplayer session
# #
# TODO: Move this code to it's own file, or possibly the LevelBus # TODO: Move this code to it's own file, or possibly the LevelBus
# TODO: save settings somewhere reasonable # TODO: Save settings somewhere reasonable
# TODO: Ditch backfire and just use Firebase directly. Easier to debug, richer APIs (E.g. presence stuff).
onRealTimeMultiplayerLevelLoaded: (session) -> onRealTimeMultiplayerLevelLoaded: ->
return if @realTimePlayerStatus?
return if me.get('anonymous') return if me.get('anonymous')
players = new RealTimeCollection('multiplayer_players/' + @levelID) unless @options.realTimeMultiplayerSessionID?
players.create players = new RealTimeCollection('multiplayer_players/' + @levelID)
id: me.id players.create
name: me.get('name') id: me.id
state: 'playing' name: me.get('name')
created: new Date().toISOString() state: 'playing'
heartbeat: new Date().toISOString() created: new Date().toISOString()
heartbeat: new Date().toISOString()
@realTimePlayerStatus = new RealTimeModel('multiplayer_players/' + @levelID + '/' + me.id) @realTimePlayerStatus = new RealTimeModel('multiplayer_players/' + @levelID + '/' + me.id)
@timerMultiplayerHeartbeatID = setInterval @onRealTimeMultiplayerHeartbeat, 60 * 1000 @timerMultiplayerHeartbeatID = setInterval @onRealTimeMultiplayerHeartbeat, 60 * 1000
@cleanupRealTimeSessions() @cleanupRealTimeSessions()
@ -608,6 +626,7 @@ module.exports = class PlayLevelView extends RootView
@realTimeSessionCollection.each @cleanupRealTimeSession @realTimeSessionCollection.each @cleanupRealTimeSession
cleanupRealTimeSession: (session) => cleanupRealTimeSession: (session) =>
return if @options.realTimeMultiplayerSessionID? and @options.realTimeMultiplayerSessionID is session.id
if session.get('state') isnt 'finished' if session.get('state') isnt 'finished'
players = new RealTimeCollection 'multiplayer_level_sessions/' + session.id + '/players' players = new RealTimeCollection 'multiplayer_level_sessions/' + session.id + '/players'
players.each (player) => players.each (player) =>
@ -618,21 +637,34 @@ module.exports = class PlayLevelView extends RootView
session.set 'state', 'finished' session.set 'state', 'finished'
onRealTimeMultiplayerLevelUnloaded: -> onRealTimeMultiplayerLevelUnloaded: ->
clearInterval @timerMultiplayerHeartbeatID if @timerMultiplayerHeartbeatID? # console.log 'PlayLevelView onRealTimeMultiplayerLevelUnloaded'
if @timerMultiplayerHeartbeatID?
clearInterval @timerMultiplayerHeartbeatID
@timerMultiplayerHeartbeatID = null
if @realTimeSessionCollection? if @realTimeSessionCollection?
@realTimeSessionCollection.off 'add', @cleanupRealTimeSession @realTimeSessionCollection.off 'add', @cleanupRealTimeSession
@realTimeSessionCollection = null @realTimeSessionCollection = null
# TODO: similar to game ending cleanup
if @realTimeOpponent?
@realTimeOpponent.off 'change', @onRealTimeOpponentChanged
@realTimeOpponent = null
if @realTimePlayers?
@realTimePlayers.off 'add', @onRealTimePlayerAdded
@realTimePlayers = null
if @realTimeSession?
@realTimeSession.off 'change', @onRealTimeSessionChanged
@realTimeSession = null
if @realTimePlayerGameStatus?
@realTimePlayerGameStatus = null
if @realTimePlayerStatus?
@realTimePlayerStatus = null
onRealTimeMultiplayerHeartbeat: => onRealTimeMultiplayerHeartbeat: =>
@realTimePlayerStatus.set 'heartbeat', new Date().toISOString() if @realTimePlayerStatus @realTimePlayerStatus.set 'heartbeat', new Date().toISOString() if @realTimePlayerStatus
onRealTimeMultiplayerCreatedGame: (e) -> onRealTimeMultiplayerCreatedGame: (e) ->
# Watch external multiplayer session @joinRealTimeMultiplayerGame e
@realTimeSession = new RealTimeModel 'multiplayer_level_sessions/' + e.session.id
@realTimeSession.on 'change', @onRealTimeSessionChanged
@realTimePlayers = new RealTimeCollection 'multiplayer_level_sessions/' + e.session.id + '/players'
@realTimePlayers.on 'add', @onRealTimePlayerAdded
@realTimePlayerGameStatus = new RealTimeModel 'multiplayer_level_sessions/' + e.session.id + '/players/' + me.id
@realTimePlayerGameStatus.set 'state', 'coding' @realTimePlayerGameStatus.set 'state', 'coding'
@realTimePlayerStatus.set 'state', 'available' @realTimePlayerStatus.set 'state', 'available'
Backbone.Mediator.publish 'real-time-multiplayer:player-status', status: 'Waiting for opponent..' Backbone.Mediator.publish 'real-time-multiplayer:player-status', status: 'Waiting for opponent..'
@ -662,33 +694,72 @@ module.exports = class PlayLevelView extends RootView
console.info 'Real-time multiplayer opponent left the game' console.info 'Real-time multiplayer opponent left the game'
opponentID = @realTimeOpponent.id opponentID = @realTimeOpponent.id
@realTimeGameEnded() @realTimeGameEnded()
Backbone.Mediator.publish 'real-time-multiplayer:left-game', id: opponentID Backbone.Mediator.publish 'real-time-multiplayer:left-game', userID: opponentID
when 'submitted' when 'submitted'
# TODO: What should this message say? # TODO: What should this message say?
Backbone.Mediator.publish 'real-time-multiplayer:player-status', status: @realTimeOpponent.get('name') + ' waiting for your code..' Backbone.Mediator.publish 'real-time-multiplayer:player-status', status: @realTimeOpponent.get('name') + ' waiting for your code'
joinRealTimeMultiplayerGame: (e) ->
unless @realTimeSession?
# TODO: Necessary for real-time multiplayer sessions?
@session.set('submittedCodeLanguage', @session.get('codeLanguage'))
@session.save()
@realTimeSession = new RealTimeModel 'multiplayer_level_sessions/' + e.realTimeSessionID
@realTimeSession.on 'change', @onRealTimeSessionChanged
@realTimePlayers = new RealTimeCollection 'multiplayer_level_sessions/' + e.realTimeSessionID + '/players'
@realTimePlayers.on 'add', @onRealTimePlayerAdded
@realTimePlayerGameStatus = new RealTimeModel 'multiplayer_level_sessions/' + e.realTimeSessionID + '/players/' + me.id
# TODO: Follow up in MultiplayerView to see if double joins can be avoided
# else
# console.error 'Joining real-time multiplayer game with an existing @realTimeSession.'
onRealTimeMultiplayerJoinedGame: (e) -> onRealTimeMultiplayerJoinedGame: (e) ->
# console.log 'PlayLevelView onRealTimeMultiplayerJoinedGame', e # console.log 'PlayLevelView onRealTimeMultiplayerJoinedGame', e
if e.id is me.id @joinRealTimeMultiplayerGame e
@realTimeSession = new RealTimeModel 'multiplayer_level_sessions/' + e.session.id @realTimePlayerGameStatus.set 'state', 'coding'
@realTimeSession.set 'state', 'coding' @realTimePlayerStatus.set 'state', 'unavailable'
@realTimeSession.on 'change', @onRealTimeSessionChanged unless @realTimeOpponent?
@realTimePlayers = new RealTimeCollection 'multiplayer_level_sessions/' + e.session.id + '/players' for id, player of @realTimeSession.get('players')
@realTimePlayers.on 'add', @onRealTimePlayerAdded
@realTimePlayerGameStatus = new RealTimeModel 'multiplayer_level_sessions/' + e.session.id + '/players/' + me.id
@realTimePlayerGameStatus.set 'state', 'coding'
@realTimePlayerStatus.set 'state', 'unavailable'
for id, player of e.session.get('players')
if id isnt me.id if id isnt me.id
@realTimeOpponent = new RealTimeModel 'multiplayer_level_sessions/' + e.session.id + '/players/' + id @realTimeOpponent = new RealTimeModel 'multiplayer_level_sessions/' + e.realTimeSessionID + '/players/' + id
@realTimeOpponent.on 'change', @onRealTimeOpponentChanged
Backbone.Mediator.publish 'real-time-multiplayer:player-status', status: 'Playing against ' + player.name Backbone.Mediator.publish 'real-time-multiplayer:player-status', status: 'Playing against ' + player.name
unless @realTimeOpponent?
console.error 'Did not find an oppoonent in onRealTimeMultiplayerJoinedGame.'
@updateTeam()
onRealTimeMultiplayerLeftGame: (e) -> onRealTimeMultiplayerLeftGame: (e) ->
# console.log 'PlayLevelView onRealTimeMultiplayerLeftGame', e # console.log 'PlayLevelView onRealTimeMultiplayerLeftGame', e
if e.id? and e.id is me.id if e.userID? and e.userID is me.id
@realTimePlayerGameStatus.set 'state', 'left' @realTimePlayerGameStatus.set 'state', 'left'
@realTimeGameEnded() @realTimeGameEnded()
realTimeMultiplayerContinueGame: (realTimeSessionID) ->
# console.log 'PlayLevelView realTimeMultiplayerContinueGame', realTimeSessionID, me.id
Backbone.Mediator.publish 'real-time-multiplayer:joined-game', realTimeSessionID: realTimeSessionID
console.info 'Setting my game status to ready'
@realTimePlayerGameStatus.set 'state', 'ready'
if @realTimeOpponent.get('state') is 'ready'
@realTimeOpponentIsReady()
else
console.info 'Waiting for opponent to be ready'
@realTimeOpponent.on 'change', @realTimeOpponentMaybeReady
realTimeOpponentMaybeReady: =>
# console.log 'PlayLevelView realTimeOpponentMaybeReady'
if @realTimeOpponent.get('state') is 'ready'
@realTimeOpponent.off 'change', @realTimeOpponentMaybeReady
@realTimeOpponentIsReady()
realTimeOpponentIsReady: =>
console.info 'All real-time multiplayer players are ready!'
@realTimeSession.set 'state', 'running'
Backbone.Mediator.publish 'real-time-multiplayer:player-status', status: 'Battling ' + @realTimeOpponent.get('name')
Backbone.Mediator.publish 'tome:manual-cast', {realTime: true}
realTimeGameEnded: -> realTimeGameEnded: ->
if @realTimeOpponent? if @realTimeOpponent?
@realTimeOpponent.off 'change', @onRealTimeOpponentChanged @realTimeOpponent.off 'change', @onRealTimeOpponentChanged
@ -707,9 +778,21 @@ module.exports = class PlayLevelView extends RootView
Backbone.Mediator.publish 'real-time-multiplayer:player-status', status: '' Backbone.Mediator.publish 'real-time-multiplayer:player-status', status: ''
onRealTimeMultiplayerCast: (e) -> onRealTimeMultiplayerCast: (e) ->
# console.log 'PlayLevelView onRealTimeMultiplayerCast', e
unless @realTimeSession unless @realTimeSession
console.error 'onRealTimeMultiplayerCast without a multiplayerSession' console.error 'onRealTimeMultiplayerCast without a multiplayerSession'
return return
# Set submissionCount for created real-time multiplayer session
if me.id is @realTimeSession.get('creator')
sessionState = @session.get('state')
if sessionState?
submissionCount = sessionState.submissionCount
console.info 'Setting multiplayer submissionCount to', submissionCount
@realTimeSession.set 'submissionCount', submissionCount
else
console.error 'Failed to read sessionState in onRealTimeMultiplayerCast'
players = new RealTimeCollection('multiplayer_level_sessions/' + @realTimeSession.id + '/players') players = new RealTimeCollection('multiplayer_level_sessions/' + @realTimeSession.id + '/players')
myPlayer = opponentPlayer = null myPlayer = opponentPlayer = null
players.each (player) -> players.each (player) ->
@ -719,10 +802,8 @@ module.exports = class PlayLevelView extends RootView
opponentPlayer = player opponentPlayer = player
if myPlayer if myPlayer
console.info 'Submitting my code' console.info 'Submitting my code'
myPlayer.set 'code', @session.get('code') @session.patch()
myPlayer.set 'codeLanguage', @session.get('codeLanguage')
myPlayer.set 'state', 'submitted' myPlayer.set 'state', 'submitted'
myPlayer.set 'team', me.team
else else
console.error 'Did not find my player in onRealTimeMultiplayerCast' console.error 'Did not find my player in onRealTimeMultiplayerCast'
if opponentPlayer if opponentPlayer
@ -738,6 +819,9 @@ module.exports = class PlayLevelView extends RootView
state = opponentPlayer.get('state') state = opponentPlayer.get('state')
if state in ['submitted', 'ready'] if state in ['submitted', 'ready']
@realTimeOpponentSubmittedCode opponentPlayer, myPlayer @realTimeOpponentSubmittedCode opponentPlayer, myPlayer
opponentPlayer.off 'change'
else
console.error 'Did not find opponent player in onRealTimeMultiplayerCast'
onRealTimeMultiplayerPlaybackEnded: -> onRealTimeMultiplayerPlaybackEnded: ->
if @realTimeSession? if @realTimeSession?
@ -747,19 +831,77 @@ module.exports = class PlayLevelView extends RootView
Backbone.Mediator.publish 'real-time-multiplayer:player-status', status: 'Playing against ' + @realTimeOpponent.get('name') Backbone.Mediator.publish 'real-time-multiplayer:player-status', status: 'Playing against ' + @realTimeOpponent.get('name')
realTimeOpponentSubmittedCode: (opponentPlayer, myPlayer) => realTimeOpponentSubmittedCode: (opponentPlayer, myPlayer) =>
# Save opponent's code # console.log 'PlayLevelView realTimeOpponentSubmittedCode', @realTimeSession.id, opponentPlayer.get('level_session')
Backbone.Mediator.publish 'real-time-multiplayer:new-opponent-code', {codeLanguage: opponentPlayer.get('codeLanguage'), code: opponentPlayer.get('code'), team: opponentPlayer.get('team')} # Read submissionCount for joined real-time multiplayer session
# I'm ready to rumble if me.id isnt @realTimeSession.get('creator')
myPlayer.set 'state', 'ready' sessionState = @session.get('state') ? {}
if opponentPlayer.get('state') is 'ready' newSubmissionCount = @realTimeSession.get 'submissionCount'
console.info 'All real-time multiplayer players are ready!' if newSubmissionCount?
@realTimeSession.set 'state', 'running' # TODO: This isn't always getting updated where the random seed generation uses it.
Backbone.Mediator.publish 'real-time-multiplayer:player-status', status: 'Battling ' + @realTimeOpponent.get('name') sessionState.submissionCount = parseInt newSubmissionCount
else console.info 'Got multiplayer submissionCount', sessionState.submissionCount
# Wait for opponent to be ready @session.set 'state', sessionState
opponentPlayer.on 'change', (e) => @session.patch()
if opponentPlayer.get('state') is 'ready'
opponentPlayer.off 'change' # Reload this level so the opponent session can easily be wired up
console.info 'All real-time multiplayer players are ready!' Backbone.Mediator.publish 'router:navigate',
@realTimeSession.set 'state', 'running' route: "/play/level/#{@levelID}"
Backbone.Mediator.publish 'real-time-multiplayer:player-status', status: 'Battling ' + @realTimeOpponent.get('name') viewClass: PlayLevelView
viewArgs: [{supermodel: @supermodel, autoUnveil: true, realTimeMultiplayerSessionID: @realTimeSession.id, opponent: opponentPlayer.get('level_session'), team: @team}, @levelID]
updateTeam: ->
# If not creator, and same team as creator, then switch teams
# TODO: Assumes there are only 'humans' and 'ogres'
unless @realTimeOpponent?
console.error 'Tried to switch teams without a real-time opponent.'
return
unless @realTimeSession?
console.error 'Tried to switch teams without a real-time session.'
return
return if me.id is @realTimeSession.get('creator')
oldTeam = @realTimeOpponent.get('team')
return unless oldTeam is @session.get('team')
# Need to switch to other team
newTeam = if oldTeam is 'humans' then 'ogres' else 'humans'
console.info "Switching from team #{oldTeam} to #{newTeam}"
# Move code from old team to new team
# Assumes teamSpells has matching spells for each team
# TODO: Similar to code in loadOpponentTeam, consolidate?
code = @session.get 'code'
teamSpells = @session.get 'teamSpells'
for oldSpellKey in teamSpells[oldTeam]
[oldThang, oldSpell] = oldSpellKey.split '/'
oldCode = code[oldThang]?[oldSpell]
continue unless oldCode?
# Move oldCode to new team under same spell
for newSpellKey in teamSpells[newTeam]
[newThang, newSpell] = newSpellKey.split '/'
if newSpell is oldSpell
# Found spell location under new team
console.log "Swapping spell=#{oldSpell} from #{oldThang} to #{newThang}"
if code[newThang]?[oldSpell]?
# Option 1: have a new spell to swap
code[oldThang][oldSpell] = code[newThang][oldSpell]
else
# Option 2: no new spell to swap
delete code[oldThang][oldSpell]
code[newThang] = {} unless code[newThang]?
code[newThang][oldSpell] = oldCode
break
@setTeam newTeam # Sets @session 'team'
sessionState = @session.get('state')
if sessionState?
# TODO: Don't hard code thangID
sessionState.selected = if newTeam is 'humans' then 'Hero Placeholder' else 'Hero Placeholder 1'
@session.set 'state', sessionState
@session.set 'code', code
@session.patch()
if sessionState?
# TODO: Don't hardcode spellName
Backbone.Mediator.publish 'level:select-sprite', thangID: sessionState.selected, spellName: 'plan'

View file

@ -63,14 +63,8 @@ module.exports = class CastButtonView extends CocoView
Backbone.Mediator.publish 'tome:manual-cast', {} Backbone.Mediator.publish 'tome:manual-cast', {}
onCastRealTimeButtonClick: (e) -> onCastRealTimeButtonClick: (e) ->
if @multiplayerSession if @inRealTimeMultiplayerSession
Backbone.Mediator.publish 'real-time-multiplayer:manual-cast', {} 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 'change'
Backbone.Mediator.publish 'tome:manual-cast', {realTime: true}
else else
Backbone.Mediator.publish 'tome:manual-cast', {realTime: true} Backbone.Mediator.publish 'tome:manual-cast', {realTime: true}
@ -149,12 +143,10 @@ module.exports = class CastButtonView extends CocoView
$(@).toggleClass('selected', parseInt($(@).attr('data-delay')) is delay) $(@).toggleClass('selected', parseInt($(@).attr('data-delay')) is delay)
onJoinedRealTimeMultiplayerGame: (e) -> onJoinedRealTimeMultiplayerGame: (e) ->
@multiplayerSession = e.session @inRealTimeMultiplayerSession = true
onLeftRealTimeMultiplayerGame: (e) -> onLeftRealTimeMultiplayerGame: (e) ->
if @multiplayerSession @inRealTimeMultiplayerSession = false
@multiplayerSession.off 'change'
@multiplayerSession = null
initButtonTextABTest: -> initButtonTextABTest: ->
return if me.isAdmin() return if me.isAdmin()

View file

@ -46,20 +46,18 @@ module.exports = class Spell
@source = @originalSource = p.aiSource @source = @originalSource = p.aiSource
@thangs = {} @thangs = {}
if @canRead() # We can avoid creating these views if we'll never use them. if @canRead() # We can avoid creating these views if we'll never use them.
@view = new SpellView {spell: @, level: options.level, session: @session, worker: @worker} @view = new SpellView {spell: @, level: options.level, session: @session, otherSession: @otherSession, worker: @worker}
@view.render() # Get it ready and code loaded in advance @view.render() # Get it ready and code loaded in advance
@tabView = new SpellListTabEntryView spell: @, supermodel: @supermodel, codeLanguage: @language, level: options.level @tabView = new SpellListTabEntryView spell: @, supermodel: @supermodel, codeLanguage: @language, level: options.level
@tabView.render() @tabView.render()
@team = @permissions.readwrite[0] ? 'common' @team = @permissions.readwrite[0] ? 'common'
Backbone.Mediator.publish 'tome:spell-created', spell: @ Backbone.Mediator.publish 'tome:spell-created', spell: @
Backbone.Mediator.subscribe 'real-time-multiplayer:new-opponent-code', @onNewOpponentCode, @
destroy: -> destroy: ->
@view?.destroy() @view?.destroy()
@tabView?.destroy() @tabView?.destroy()
@thangs = null @thangs = null
@worker = null @worker = null
Backbone.Mediator.unsubscribe 'real-time-multiplayer:new-opponent-code', @onNewOpponentCode, @
setLanguage: (@language) -> setLanguage: (@language) ->
#console.log 'setting language to', @language, 'so using original source', @languages[language] ? @languages.javascript #console.log 'setting language to', @language, 'so using original source', @languages[language] ? @languages.javascript
@ -187,22 +185,13 @@ module.exports = class Spell
shouldUseTranspiledCode: -> shouldUseTranspiledCode: ->
# Determine whether this code has already been transpiled, or whether it's raw source needing transpilation. # Determine whether this code has already been transpiled, or whether it's raw source needing transpilation.
return false if @levelType is 'hero-ladder'
return true if @spectateView # Use transpiled code for both teams if we're just spectating. return true if @spectateView # Use transpiled code for both teams if we're just spectating.
return true if @isEnemySpell() # Use transpiled for enemy spells. return true if @isEnemySpell() # Use transpiled for enemy spells.
# Players without permissions can't view the raw code. # 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)) return true if @session.get('creator') isnt me.id and not (me.isAdmin() or 'employer' in me.get('permissions', true))
false false
onNewOpponentCode: (e) ->
return unless @spellKey and @canWrite e.team
if e.codeLanguage and e.code
[thangSlug, methodSlug] = @spellKey.split '/'
if opponentCode = e.code[thangSlug]?[methodSlug]
@source = opponentCode
@updateLanguageAether e.codeLanguage
else
console.error 'Spell onNewOpponentCode did not receive code', e
createProblemContext: (thang) -> createProblemContext: (thang) ->
# Create problemContext Aether can use to craft better error messages # Create problemContext Aether can use to craft better error messages
# stringReferences: values that should be referred to as a string instead of a variable (e.g. "Brak", not Brak) # stringReferences: values that should be referred to as a string instead of a variable (e.g. "Brak", not Brak)

View file

@ -237,8 +237,12 @@ module.exports = class TomeView extends CocoView
@cast() @cast()
onSelectPrimarySprite: (e) -> onSelectPrimarySprite: (e) ->
# TODO: this may not be correct # This is only fired by PlayLevelView for hero levels currently
Backbone.Mediator.publish 'level:select-sprite', thangID: 'Hero Placeholder' # TODO: Don't hard code these hero names
if @options.session.get('team') is 'ogres'
Backbone.Mediator.publish 'level:select-sprite', thangID: 'Hero Placeholder 1'
else
Backbone.Mediator.publish 'level:select-sprite', thangID: 'Hero Placeholder'
destroy: -> destroy: ->
spell.destroy() for spellKey, spell of @spells spell.destroy() for spellKey, spell of @spells

View file

@ -1,5 +1,7 @@
ModalView = require 'views/kinds/ModalView' ModalView = require 'views/kinds/ModalView'
template = require 'templates/play/modal/buy-gems-modal' template = require 'templates/play/modal/buy-gems-modal'
stripeHandler = require 'lib/services/stripe'
utils = require 'lib/utils'
module.exports = class BuyGemsModal extends ModalView module.exports = class BuyGemsModal extends ModalView
id: 'buy-gems-modal' id: 'buy-gems-modal'
@ -7,20 +9,22 @@ module.exports = class BuyGemsModal extends ModalView
plain: true plain: true
originalProducts: [ originalProducts: [
{ price: '$4.99', gems: 5000, id: 'gems_5', i18n: 'buy_gems.few_gems' } { price: '$4.99', gems: 5000, amount: 499, id: 'gems_5', i18n: 'buy_gems.few_gems' }
{ price: '$9.99', gems: 11000, id: 'gems_10', i18n: 'buy_gems.pile_gems' } { price: '$9.99', gems: 11000, amount: 999, id: 'gems_10', i18n: 'buy_gems.pile_gems' }
{ price: '$19.99', gems: 25000, id: 'gems_20', i18n: 'buy_gems.chest_gems' } { price: '$19.99', gems: 25000, amount: 1999, id: 'gems_20', i18n: 'buy_gems.chest_gems' }
] ]
subscriptions: subscriptions:
'ipad:products': 'onIPadProducts' 'ipad:products': 'onIPadProducts'
'ipad:iap-complete': 'onIAPComplete' 'ipad:iap-complete': 'onIAPComplete'
'stripe:received-token': 'onStripeReceivedToken'
events: events:
'click .product button': 'onClickProductButton' 'click .product button': 'onClickProductButton'
constructor: (options) -> constructor: (options) ->
super(options) super(options)
@state = 'standby'
if application.isIPadApp if application.isIPadApp
@products = [] @products = []
Backbone.Mediator.publish 'buy-gems-modal:update-products' Backbone.Mediator.publish 'buy-gems-modal:update-products'
@ -30,6 +34,7 @@ module.exports = class BuyGemsModal extends ModalView
getRenderData: -> getRenderData: ->
c = super() c = super()
c.products = @products c.products = @products
c.state = @state
return c return c
onIPadProducts: (e) -> onIPadProducts: (e) ->
@ -43,15 +48,48 @@ module.exports = class BuyGemsModal extends ModalView
@render() @render()
onClickProductButton: (e) -> onClickProductButton: (e) ->
productID = $(e.target).closest('button.product').val() productID = $(e.target).closest('button').val()
console.log 'purchasing', _.find @products, { id: productID } product = _.find @products, { id: productID }
if application.isIPadApp if application.isIPadApp
Backbone.Mediator.publish 'buy-gems-modal:purchase-initiated', { productID: productID } Backbone.Mediator.publish 'buy-gems-modal:purchase-initiated', { productID: productID }
else else
application.tracker?.trackEvent 'Started purchase', {productID:productID}, ['Google Analytics'] application.tracker?.trackEvent 'Started purchase', {productID:productID}, ['Google Analytics']
alert('Not yet implemented, but thanks for trying!') stripeHandler.open({
description: $.t(product.i18n)
amount: product.amount
})
@productBeingPurchased = product
onStripeReceivedToken: (e) ->
@timestampForPurchase = new Date().getTime()
data = {
productID: @productBeingPurchased.id
stripe: {
token: e.token.id
timestamp: @timestampForPurchase
}
}
@state = 'purchasing'
@render()
jqxhr = $.post('/db/payment', data)
jqxhr.done(=>
document.location.reload()
)
jqxhr.fail(=>
if jqxhr.status is 402
@state = 'declined'
@stateMessage = arguments[2]
else if jqxhr.status is 500
@state = 'retrying'
f = _.bind @onStripeReceivedToken, @, e
_.delay f, 2000
else
@state = 'unknown_error'
@render()
)
onIAPComplete: (e) -> onIAPComplete: (e) ->
product = _.find @products, { id: e.productID } product = _.find @products, { id: e.productID }

View file

@ -69,7 +69,8 @@
"aether": "~0.2.39", "aether": "~0.2.39",
"JASON": "~0.1.3", "JASON": "~0.1.3",
"JQDeferred": "~2.1.0", "JQDeferred": "~2.1.0",
"jsondiffpatch": "0.1.17" "jsondiffpatch": "0.1.17",
"stripe": "~2.9.0"
}, },
"devDependencies": { "devDependencies": {
"jade": "0.33.x", "jade": "0.33.x",

View file

@ -68,10 +68,6 @@ LevelHandler = class LevelHandler extends Handler
if req.query.team? if req.query.team?
sessionQuery.team = req.query.team sessionQuery.team = req.query.team
# TODO: generalize this for levels based on their teams
else if level.get('type') in ['ladder', 'hero-ladder']
sessionQuery.team = 'humans'
Session.findOne(sessionQuery).exec (err, doc) => Session.findOne(sessionQuery).exec (err, doc) =>
return @sendDatabaseError(res, err) if err return @sendDatabaseError(res, err) if err
return @sendSuccess(res, doc) if doc? return @sendSuccess(res, doc) if doc?

View file

@ -38,7 +38,8 @@ LevelSessionSchema.pre 'save', (next) ->
LevelSessionSchema.statics.privateProperties = ['code', 'submittedCode', 'unsubscribed'] LevelSessionSchema.statics.privateProperties = ['code', 'submittedCode', 'unsubscribed']
LevelSessionSchema.statics.editableProperties = ['multiplayer', 'players', 'code', 'codeLanguage', 'completed', 'state', LevelSessionSchema.statics.editableProperties = ['multiplayer', 'players', 'code', 'codeLanguage', 'completed', 'state',
'levelName', 'creatorName', 'levelID', 'screenshot', 'levelName', 'creatorName', 'levelID', 'screenshot',
'chat', 'teamSpells', 'submitted', 'submittedCodeLanguage', 'unsubscribed', 'playtime', 'heroConfig'] 'chat', 'teamSpells', 'submitted', 'submittedCodeLanguage',
'unsubscribed', 'playtime', 'heroConfig', 'team']
LevelSessionSchema.statics.jsonSchema = jsonschema LevelSessionSchema.statics.jsonSchema = jsonschema
LevelSessionSchema.index {user: 1, changed: -1}, {sparse: true, name: 'last played index'} LevelSessionSchema.index {user: 1, changed: -1}, {sparse: true, name: 'last played index'}

View file

@ -8,21 +8,26 @@ sendwithus = require '../sendwithus'
hipchat = require '../hipchat' hipchat = require '../hipchat'
config = require '../../server_config' config = require '../../server_config'
request = require 'request' request = require 'request'
stripe = require('stripe')(config.stripe.secretKey)
async = require 'async'
products = { products = {
'gems_5': { 'gems_5': {
amount: 500 amount: 499
gems: 5000 gems: 5000
id: 'gems_5'
} }
'gems_10': { 'gems_10': {
amount: 1000 amount: 999
gems: 11000 gems: 11000
id: 'gems_10'
} }
'gems_20': { 'gems_20': {
amount: 2000 amount: 1999
gems: 25000 gems: 25000
id: 'gems_20'
} }
} }
@ -45,9 +50,16 @@ PaymentHandler = class PaymentHandler extends Handler
appleLocalPrice = req.body.apple?.localPrice appleLocalPrice = req.body.apple?.localPrice
stripeToken = req.body.stripe?.token stripeToken = req.body.stripe?.token
stripeTimestamp = parseInt(req.body.stripe?.timestamp) stripeTimestamp = parseInt(req.body.stripe?.timestamp)
productID = req.body.productID
if not (appleReceipt or stripeTimestamp) if not (appleReceipt or (stripeTimestamp and productID))
return @sendBadInputError(res, 'Need either apple.rawReceipt or stripe.timestamp') return @sendBadInputError(res, 'Need either apple.rawReceipt or stripe.timestamp and productID')
if stripeTimestamp and not productID
return @sendBadInputError(res, 'Need productID if paying with Stripe.')
if stripeTimestamp and (not stripeToken) and (not user.get('stripeCustomerID'))
return @sendBadInputError(res, 'Need stripe.token if new customer.')
if appleReceipt if appleReceipt
if not appleTransactionID if not appleTransactionID
@ -55,7 +67,10 @@ PaymentHandler = class PaymentHandler extends Handler
@handleApplePaymentPost(req, res, appleReceipt, appleTransactionID, appleLocalPrice) @handleApplePaymentPost(req, res, appleReceipt, appleTransactionID, appleLocalPrice)
else else
@handleStripePaymentPost(req, res, stripeTimestamp, stripeToken) @handleStripePaymentPost(req, res, stripeTimestamp, productID, stripeToken)
#- Apple payments
handleApplePaymentPost: (req, res, receipt, transactionID, localPrice) -> handleApplePaymentPost: (req, res, receipt, transactionID, localPrice) ->
formFields = { 'receipt-data': receipt } formFields = { 'receipt-data': receipt }
@ -87,8 +102,6 @@ PaymentHandler = class PaymentHandler extends Handler
payment.set 'service', 'ios' payment.set 'service', 'ios'
product = products[transaction.product_id] product = products[transaction.product_id]
product ?= _.values(products)[0] # TEST
payment.set 'amount', product.amount payment.set 'amount', product.amount
payment.set 'gems', product.gems payment.set 'gems', product.gems
payment.set 'ios', { payment.set 'ios', {
@ -110,10 +123,123 @@ PaymentHandler = class PaymentHandler extends Handler
) )
handleStripePaymentPost: (req, res, timestamp, token) -> #- Stripe payments
console.log 'lol not implemented yet'
@sendNotFoundError(res)
handleStripePaymentPost: (req, res, timestamp, productID, token) ->
# First, make sure we save the payment info as a Customer object, if we haven't already.
if not req.user.get('stripeCustomerID')
stripe.customers.create({
card: token
description: req.user._id + ''
}).then(((customer) =>
req.user.set('stripeCustomerID', customer.id)
req.user.save((err) =>
return @sendDatabaseError(res, err) if err
@beginStripePayment(req, res, timestamp, productID)
)
),
(err) =>
return @sendDatabaseError(res, err)
)
else
@beginStripePayment(req, res, timestamp, productID)
beginStripePayment: (req, res, timestamp, productID) ->
product = products[productID]
async.parallel([
((callback) ->
criteria = { recipient: req.user._id, 'stripe.timestamp': timestamp }
Payment.findOne(criteria).exec((err, payment) =>
callback(err, payment)
)
),
((callback) ->
stripe.charges.list({customer: req.user.get('stripeCustomerID')}, (err, recentCharges) =>
return callback(err) if err
charge = _.find recentCharges.data, (c) -> c.metadata.timestamp is timestamp
callback(null, charge)
)
)
],
((err, results) =>
return @sendDatabaseError(res, err) if err
[payment, charge] = results
if not (payment or charge)
# Proceed normally from the beginning
@chargeStripe(req, res, payment, product)
else if charge and not payment
# Initialized Payment. Start from charging.
@recordStripeCharge(req, res, payment, product, charge)
else
# Charged Stripe and recorded it. Recalculate gems to make sure credited the purchase.
@recalculateGemsFor(req.user, (err) =>
return @sendDatabaseError(res, err) if err
@sendSuccess(res, @formatEntity(req, payment))
)
)
)
chargeStripe: (req, res, payment, product) ->
stripe.charges.create({
amount: product.amount
currency: 'usd'
customer: req.user.get('stripeCustomerID')
metadata: {
productID: product.id
userID: req.user._id + ''
gems: product.gems
timestamp: parseInt(req.body.stripe?.timestamp)
}
receipt_email: req.user.get('email')
}).then(
# success case
((charge) => @recordStripeCharge(req, res, payment, product, charge)),
# error case
((err) =>
if err.type in ['StripeCardError', 'StripeInvalidRequestError']
@sendError(res, 402, err.message)
else
@sendDatabaseError(res, 'Error charging card, please retry.'))
)
recordStripeCharge: (req, res, payment, product, charge) ->
return @sendError(res, 500, 'Fake db error for testing.') if req.body.breakAfterCharging
payment = @makeNewInstance(req)
payment.set 'service', 'stripe'
payment.set 'productID', req.body.productID
payment.set 'amount', product.amount
payment.set 'gems', product.gems
payment.set 'stripe', {
customerID: req.user.get('stripeCustomerID')
timestamp: parseInt(req.body.stripe.timestamp)
chargeID: charge.id
}
validation = @validateDocumentInput(payment.toObject())
return @sendBadInputError(res, validation.errors) if validation.valid is false
payment.save((err) =>
# Credit gems
return @sendDatabaseError(res, err) if err
@incrementGemsFor(req.user, product.gems, (err) =>
return @sendDatabaseError(res, err) if err
@sendCreated(res, @formatEntity(req, payment))
)
)
#- Incrementing/recalculating gems
incrementGemsFor: (user, gems, done) -> incrementGemsFor: (user, gems, done) ->
purchased = _.clone(user.get('purchased')) purchased = _.clone(user.get('purchased'))

View file

@ -124,7 +124,7 @@ module.exports.getTwoGames = (req, res) ->
#if userIsAnonymous req then return errors.unauthorized(res, 'You need to be logged in to get games.') #if userIsAnonymous req then return errors.unauthorized(res, 'You need to be logged in to get games.')
humansGameID = req.body.humansGameID humansGameID = req.body.humansGameID
ogresGameID = req.body.ogresGameID ogresGameID = req.body.ogresGameID
ladderGameIDs = ['greed', 'criss-cross', 'brawlwood', 'dungeon-arena', 'gold-rush', 'sky-span', 'dueling-grounds', 'cavern-survival', 'intuit-tf2014'] ladderGameIDs = ['greed', 'criss-cross', 'brawlwood', 'dungeon-arena', 'gold-rush', 'sky-span', 'dueling-grounds', 'cavern-survival', 'multiplayer-treasure-grove']
levelID = _.sample ladderGameIDs levelID = _.sample ladderGameIDs
unless ogresGameID and humansGameID unless ogresGameID and humansGameID
#fetch random games here #fetch random games here

View file

@ -192,7 +192,7 @@ UserSchema.statics.hashPassword = (password) ->
UserSchema.statics.privateProperties = [ UserSchema.statics.privateProperties = [
'permissions', 'email', 'mailChimp', 'firstName', 'lastName', 'gender', 'facebookID', 'permissions', 'email', 'mailChimp', 'firstName', 'lastName', 'gender', 'facebookID',
'gplusID', 'music', 'volume', 'aceConfig', 'employerAt', 'signedEmployerAgreement', 'gplusID', 'music', 'volume', 'aceConfig', 'employerAt', 'signedEmployerAgreement',
'emailSubscriptions', 'emails', 'activity' 'emailSubscriptions', 'emails', 'activity', 'stripeCustomerID'
] ]
UserSchema.statics.jsonSchema = jsonschema UserSchema.statics.jsonSchema = jsonschema
UserSchema.statics.editableProperties = [ UserSchema.statics.editableProperties = [

View file

@ -18,8 +18,10 @@ config.mongo =
mongoose_replica_string: process.env.COCO_MONGO_MONGOOSE_REPLICA_STRING or '' mongoose_replica_string: process.env.COCO_MONGO_MONGOOSE_REPLICA_STRING or ''
config.apple = config.apple =
#verifyURL: process.env.COCO_APPLE_VERIFY_URL or 'https://sandbox.itunes.apple.com/verifyReceipt' verifyURL: process.env.COCO_APPLE_VERIFY_URL or 'https://sandbox.itunes.apple.com/verifyReceipt'
verifyURL: process.env.COCO_APPLE_VERIFY_URL or 'https://buy.itunes.apple.com/verifyReceipt'
config.stripe =
secretKey: process.env.COCO_STRIPE_SECRET_KEY or 'sk_test_MFnZHYD0ixBbiBuvTlLjl2da'
config.redis = config.redis =
port: process.env.COCO_REDIS_PORT or 6379 port: process.env.COCO_REDIS_PORT or 6379

File diff suppressed because one or more lines are too long