Merge branch 'master' into production

This commit is contained in:
Scott Erickson 2016-07-08 16:15:08 -07:00
commit 118c76cc2d
12 changed files with 97 additions and 25 deletions

View file

@ -319,6 +319,7 @@ self.setupDebugWorldToRunUntilFrame = function (args) {
self.debugWorld.submissionCount = args.submissionCount; self.debugWorld.submissionCount = args.submissionCount;
self.debugWorld.fixedSeed = args.fixedSeed; self.debugWorld.fixedSeed = args.fixedSeed;
self.debugWorld.flagHistory = args.flagHistory; self.debugWorld.flagHistory = args.flagHistory;
self.debugWorld.realTimeInputEvents = args.realTimeInputEvents;
self.debugWorld.difficulty = args.difficulty; self.debugWorld.difficulty = args.difficulty;
if (args.level) if (args.level)
self.debugWorld.loadFromLevel(args.level, true); self.debugWorld.loadFromLevel(args.level, true);
@ -381,6 +382,7 @@ self.runWorld = function runWorld(args) {
self.world.submissionCount = args.submissionCount; self.world.submissionCount = args.submissionCount;
self.world.fixedSeed = args.fixedSeed; self.world.fixedSeed = args.fixedSeed;
self.world.flagHistory = args.flagHistory || []; self.world.flagHistory = args.flagHistory || [];
self.world.realTimeInputEvents = args.realTimeInputEvents || [];
self.world.difficulty = args.difficulty || 0; self.world.difficulty = args.difficulty || 0;
if(args.level) if(args.level)
self.world.loadFromLevel(args.level, true); self.world.loadFromLevel(args.level, true);
@ -539,6 +541,11 @@ self.addFlagEvent = function addFlagEvent(flagEvent) {
self.world.addFlagEvent(flagEvent); self.world.addFlagEvent(flagEvent);
}; };
self.addRealTimeInputEvent = function addRealTimeInputEvent(realTimeInputEvent) {
if(!self.world) return;
self.world.addRealTimeInputEvent(realTimeInputEvent);
};
self.stopRealTimePlayback = function stopRealTimePlayback() { self.stopRealTimePlayback = function stopRealTimePlayback() {
if(!self.world) return; if(!self.world) return;
self.world.realTime = false; self.world.realTime = false;

View file

@ -60,6 +60,11 @@ module.exports = ModuleLoader = class ModuleLoader extends CocoClass
# load dependencies if it's not a vendor library # load dependencies if it's not a vendor library
if not _.string.startsWith(e.item.id, 'vendor') if not _.string.startsWith(e.item.id, 'vendor')
have = window.require.list() have = window.require.list()
haveWithIndexRemoved = _(have)
.filter (file) -> _.string.endsWith(file, 'index')
.map (file) -> file.slice(0,-6)
.value()
have = have.concat(haveWithIndexRemoved)
console.group('Dependencies', e.item.id) if LOG console.group('Dependencies', e.item.id) if LOG
@recentLoadedBytes += e.rawResult.length @recentLoadedBytes += e.rawResult.length
dependencies = @parseDependencies(e.rawResult) dependencies = @parseDependencies(e.rawResult)

View file

@ -37,6 +37,7 @@ module.exports = class Angel extends CocoClass
@allLogs = [] @allLogs = []
@hireWorker() @hireWorker()
@shared.angels.push @ @shared.angels.push @
@listenTo @shared.gameUIState.get('realTimeInputEvents'), 'add', @onAddRealTimeInputEvent
destroy: -> destroy: ->
@fireWorker false @fireWorker false
@ -266,6 +267,10 @@ module.exports = class Angel extends CocoClass
return unless @running and @work.realTime return unless @running and @work.realTime
@worker.postMessage func: 'addFlagEvent', args: e @worker.postMessage func: 'addFlagEvent', args: e
onAddRealTimeInputEvent: (realTimeInputEvent) ->
return unless @running and @work.realTime
@worker.postMessage func: 'addRealTimeInputEvent', args: realTimeInputEvent.toJSON()
onStopRealTimePlayback: (e) -> onStopRealTimePlayback: (e) ->
return unless @running and @work.realTime return unless @running and @work.realTime
@work.realTime = false @work.realTime = false

View file

@ -6,6 +6,7 @@
World = require 'lib/world/world' World = require 'lib/world/world'
CocoClass = require 'core/CocoClass' CocoClass = require 'core/CocoClass'
Angel = require 'lib/Angel' Angel = require 'lib/Angel'
GameUIState = require 'models/GameUIState'
module.exports = class God extends CocoClass module.exports = class God extends CocoClass
@nicks: ['Athena', 'Baldr', 'Crom', 'Dagr', 'Eris', 'Freyja', 'Great Gish', 'Hades', 'Ishtar', 'Janus', 'Khronos', 'Loki', 'Marduk', 'Negafook', 'Odin', 'Poseidon', 'Quetzalcoatl', 'Ra', 'Shiva', 'Thor', 'Umvelinqangi', 'Týr', 'Vishnu', 'Wepwawet', 'Xipe Totec', 'Yahweh', 'Zeus', '上帝', 'Tiamat', '盘古', 'Phoebe', 'Artemis', 'Osiris', '嫦娥', 'Anhur', 'Teshub', 'Enlil', 'Perkele', 'Chaos', 'Hera', 'Iris', 'Theia', 'Uranus', 'Stribog', 'Sabazios', 'Izanagi', 'Ao', 'Tāwhirimātea', 'Tengri', 'Inmar', 'Torngarsuk', 'Centzonhuitznahua', 'Hunab Ku', 'Apollo', 'Helios', 'Thoth', 'Hyperion', 'Alectrona', 'Eos', 'Mitra', 'Saranyu', 'Freyr', 'Koyash', 'Atropos', 'Clotho', 'Lachesis', 'Tyche', 'Skuld', 'Urðr', 'Verðandi', 'Camaxtli', 'Huhetotl', 'Set', 'Anu', 'Allah', 'Anshar', 'Hermes', 'Lugh', 'Brigit', 'Manannan Mac Lir', 'Persephone', 'Mercury', 'Venus', 'Mars', 'Azrael', 'He-Man', 'Anansi', 'Issek', 'Mog', 'Kos', 'Amaterasu Omikami', 'Raijin', 'Susanowo', 'Blind Io', 'The Lady', 'Offler', 'Ptah', 'Anubis', 'Ereshkigal', 'Nergal', 'Thanatos', 'Macaria', 'Angelos', 'Erebus', 'Hecate', 'Hel', 'Orcus', 'Ishtar-Deela Nakh', 'Prometheus', 'Hephaestos', 'Sekhmet', 'Ares', 'Enyo', 'Otrera', 'Pele', 'Hadúr', 'Hachiman', 'Dayisun Tngri', 'Ullr', 'Lua', 'Minerva'] @nicks: ['Athena', 'Baldr', 'Crom', 'Dagr', 'Eris', 'Freyja', 'Great Gish', 'Hades', 'Ishtar', 'Janus', 'Khronos', 'Loki', 'Marduk', 'Negafook', 'Odin', 'Poseidon', 'Quetzalcoatl', 'Ra', 'Shiva', 'Thor', 'Umvelinqangi', 'Týr', 'Vishnu', 'Wepwawet', 'Xipe Totec', 'Yahweh', 'Zeus', '上帝', 'Tiamat', '盘古', 'Phoebe', 'Artemis', 'Osiris', '嫦娥', 'Anhur', 'Teshub', 'Enlil', 'Perkele', 'Chaos', 'Hera', 'Iris', 'Theia', 'Uranus', 'Stribog', 'Sabazios', 'Izanagi', 'Ao', 'Tāwhirimātea', 'Tengri', 'Inmar', 'Torngarsuk', 'Centzonhuitznahua', 'Hunab Ku', 'Apollo', 'Helios', 'Thoth', 'Hyperion', 'Alectrona', 'Eos', 'Mitra', 'Saranyu', 'Freyr', 'Koyash', 'Atropos', 'Clotho', 'Lachesis', 'Tyche', 'Skuld', 'Urðr', 'Verðandi', 'Camaxtli', 'Huhetotl', 'Set', 'Anu', 'Allah', 'Anshar', 'Hermes', 'Lugh', 'Brigit', 'Manannan Mac Lir', 'Persephone', 'Mercury', 'Venus', 'Mars', 'Azrael', 'He-Man', 'Anansi', 'Issek', 'Mog', 'Kos', 'Amaterasu Omikami', 'Raijin', 'Susanowo', 'Blind Io', 'The Lady', 'Offler', 'Ptah', 'Anubis', 'Ereshkigal', 'Nergal', 'Thanatos', 'Macaria', 'Angelos', 'Erebus', 'Hecate', 'Hel', 'Orcus', 'Ishtar-Deela Nakh', 'Prometheus', 'Hephaestos', 'Sekhmet', 'Ares', 'Enyo', 'Otrera', 'Pele', 'Hadúr', 'Hachiman', 'Dayisun Tngri', 'Ullr', 'Lua', 'Minerva']
@ -18,15 +19,17 @@ module.exports = class God extends CocoClass
constructor: (options) -> constructor: (options) ->
options ?= {} options ?= {}
@retrieveValueFromFrame = _.throttle @retrieveValueFromFrame, 1000 @retrieveValueFromFrame = _.throttle @retrieveValueFromFrame, 1000
@gameUIState ?= options.gameUIState or new GameUIState()
super() super()
# Angels are all given access to this. # Angels are all given access to this.
@angelsShare = @angelsShare = {
workerCode: options.workerCode or '/javascripts/workers/worker_world.js' # Either path or function workerCode: options.workerCode or '/javascripts/workers/worker_world.js' # Either path or function
headless: options.headless # Whether to just simulate the goals, or to deserialize all simulation results headless: options.headless # Whether to just simulate the goals, or to deserialize all simulation results
spectate: options.spectate spectate: options.spectate
god: @ god: @
godNick: @nick godNick: @nick
@gameUIState
workQueue: [] workQueue: []
firstWorld: true firstWorld: true
world: undefined world: undefined
@ -34,6 +37,7 @@ module.exports = class God extends CocoClass
worldClassMap: undefined worldClassMap: undefined
angels: [] angels: []
busyAngels: [] # Busy angels will automatically register here. busyAngels: [] # Busy angels will automatically register here.
}
# Determine how many concurrent Angels/web workers to use at a time # Determine how many concurrent Angels/web workers to use at a time
# ~20MB per idle worker + angel overhead - every Angel maps to 1 worker # ~20MB per idle worker + angel overhead - every Angel maps to 1 worker

View file

@ -92,6 +92,8 @@ module.exports = Surface = class Surface extends CocoClass
@gameUIState = @options.gameUIState or new GameUIState({ @gameUIState = @options.gameUIState or new GameUIState({
canDragCamera: true canDragCamera: true
}) })
@realTimeInputEvents = @gameUIState.get('realTimeInputEvents')
@listenTo(@gameUIState, 'sprite:mouse-down', @onSpriteMouseDown)
@initEasel() @initEasel()
@initAudio() @initAudio()
@onResize = _.debounce @onResize, resizeDelay @onResize = _.debounce @onResize, resizeDelay
@ -510,6 +512,15 @@ module.exports = Surface = class Surface extends CocoClass
@gameUIState.trigger('surface:stage-mouse-down', event) @gameUIState.trigger('surface:stage-mouse-down', event)
@mouseIsDown = true @mouseIsDown = true
onSpriteMouseDown: (e) =>
return unless @realTime
@realTimeInputEvents.add({
type: 'mousedown'
pos: @camera.screenToWorld x: e.originalEvent.stageX, y: e.originalEvent.stageY
time: @world.dt * @world.frames.length
thangID: e.sprite.thang.id
})
onMouseUp: (e) => onMouseUp: (e) =>
return if @disabled return if @disabled
onBackground = not @webGLStage.hitTest e.stageX, e.stageY onBackground = not @webGLStage.hitTest e.stageX, e.stageY
@ -596,6 +607,7 @@ module.exports = Surface = class Surface extends CocoClass
onRealTimePlaybackStarted: (e) -> onRealTimePlaybackStarted: (e) ->
return if @realTime return if @realTime
@realTimeInputEvents.reset()
@realTime = true @realTime = true
@onResize() @onResize()
@playing = false # Will start when countdown is done. @playing = false # Will start when countdown is done.

View file

@ -216,6 +216,9 @@ module.exports = class World
addFlagEvent: (flagEvent) -> addFlagEvent: (flagEvent) ->
@flagHistory.push flagEvent @flagHistory.push flagEvent
addRealTimeInputEvent: (realTimeInputEvent) ->
@realTimeInputEvents.push realTimeInputEvent
loadFromLevel: (level, willSimulate=true) -> loadFromLevel: (level, willSimulate=true) ->
@levelID = level.slug @levelID = level.slug
@levelComponents = level.levelComponents @levelComponents = level.levelComponents

View file

@ -26,4 +26,5 @@ module.exports = class GameUIState extends CocoModel
defaults: -> { defaults: -> {
selected: [] selected: []
canDragCamera: true canDragCamera: true
realTimeInputEvents: new Backbone.Collection()
} }

View file

@ -48,6 +48,7 @@ module.exports = class VerifierTest extends CocoClass
session.set 'code', {'hero-placeholder': plan: session.solution.source} session.set 'code', {'hero-placeholder': plan: session.solution.source}
state = session.get 'state' state = session.get 'state'
state.flagHistory = session.solution.flagHistory state.flagHistory = session.solution.flagHistory
state.realTimeInputEvents = session.solution.realTimeInputEvents
state.difficulty = session.solution.difficulty or 0 state.difficulty = session.solution.difficulty or 0
session.solution.seed = undefined unless _.isNumber session.solution.seed # TODO: migrate away from submissionCount/sessionID seed objects session.solution.seed = undefined unless _.isNumber session.solution.seed # TODO: migrate away from submissionCount/sessionID seed objects
catch e catch e

View file

@ -20,6 +20,7 @@ Article = require 'models/Article'
Camera = require 'lib/surface/Camera' Camera = require 'lib/surface/Camera'
AudioPlayer = require 'lib/AudioPlayer' AudioPlayer = require 'lib/AudioPlayer'
Simulator = require 'lib/simulator/Simulator' Simulator = require 'lib/simulator/Simulator'
GameUIState = require 'models/GameUIState'
# subviews # subviews
LevelLoadingView = require './LevelLoadingView' LevelLoadingView = require './LevelLoadingView'
@ -108,6 +109,7 @@ module.exports = class PlayLevelView extends RootView
@opponentSessionID = @getQueryVariable('opponent') @opponentSessionID = @getQueryVariable('opponent')
@opponentSessionID ?= @options.opponent @opponentSessionID ?= @options.opponent
@gameUIState = new GameUIState()
$(window).on 'resize', @onWindowResize $(window).on 'resize', @onWindowResize
@saveScreenshot = _.throttle @saveScreenshot, 30000 @saveScreenshot = _.throttle @saveScreenshot, 30000
@ -137,7 +139,7 @@ module.exports = class PlayLevelView extends RootView
load: -> load: ->
@loadStartTime = new Date() @loadStartTime = new Date()
@god = new God() @god = new God({@gameUIState})
levelLoaderOptions = supermodel: @supermodel, levelID: @levelID, sessionID: @sessionID, opponentSessionID: @opponentSessionID, team: @getQueryVariable('team'), observing: @observing, courseID: @courseID levelLoaderOptions = supermodel: @supermodel, levelID: @levelID, sessionID: @sessionID, opponentSessionID: @opponentSessionID, team: @getQueryVariable('team'), observing: @observing, courseID: @courseID
if me.isSessionless() if me.isSessionless()
levelLoaderOptions.fakeSessionConfig = {} levelLoaderOptions.fakeSessionConfig = {}
@ -345,12 +347,14 @@ module.exports = class PlayLevelView extends RootView
initSurface: -> initSurface: ->
webGLSurface = $('canvas#webgl-surface', @$el) webGLSurface = $('canvas#webgl-surface', @$el)
normalSurface = $('canvas#normal-surface', @$el) normalSurface = $('canvas#normal-surface', @$el)
surfaceOptions = surfaceOptions = {
thangTypes: @supermodel.getModels(ThangType) thangTypes: @supermodel.getModels(ThangType)
observing: @observing @observing
playerNames: @findPlayerNames() playerNames: @findPlayerNames()
levelType: @level.get('type', true) levelType: @level.get('type', true)
stayVisible: @showAds() stayVisible: @showAds()
@gameUIState
}
@surface = new Surface(@world, normalSurface, webGLSurface, surfaceOptions) @surface = new Surface(@world, normalSurface, webGLSurface, surfaceOptions)
worldBounds = @world.getBounds() worldBounds = @world.getBounds()
bounds = [{x: worldBounds.left, y: worldBounds.top}, {x: worldBounds.right, y: worldBounds.bottom}] bounds = [{x: worldBounds.left, y: worldBounds.top}, {x: worldBounds.right, y: worldBounds.bottom}]

View file

@ -202,7 +202,7 @@ module.exports = class TomeView extends CocoView
spell = @spellFor thang, spellName spell = @spellFor thang, spellName
unless spell?.canRead() unless spell?.canRead()
@clearSpellView() @clearSpellView()
@updateSpellPalette thang, spell @updateSpellPalette thang, spell if spell
return return
unless spell.view is @spellView unless spell.view is @spellView
@clearSpellView() @clearSpellView()

View file

@ -13,6 +13,8 @@ User = require '../models/User'
Classroom = require '../models/Classroom' Classroom = require '../models/Classroom'
facebook = require '../lib/facebook' facebook = require '../lib/facebook'
gplus = require '../lib/gplus' gplus = require '../lib/gplus'
TrialRequest = require '../models/TrialRequest'
log = require 'winston'
module.exports = module.exports =
fetchByGPlusID: wrap (req, res, next) -> fetchByGPlusID: wrap (req, res, next) ->
@ -133,16 +135,7 @@ module.exports =
throw new errors.Conflict('Email already taken') throw new errors.Conflict('Email already taken')
req.user.set({ password, email, anonymous: false }) req.user.set({ password, email, anonymous: false })
try yield module.exports.finishSignup(req, res)
yield req.user.save()
catch e
if e.code is 11000 # Duplicate key error
throw new errors.Conflict('Email already taken')
else
throw e
req.user.sendWelcomeEmail()
res.status(200).send(req.user.toObject({req: req}))
signupWithFacebook: wrap (req, res) -> signupWithFacebook: wrap (req, res) ->
unless req.user.isAnonymous() unless req.user.isAnonymous()
@ -159,16 +152,7 @@ module.exports =
throw new errors.UnprocessableEntity('Invalid facebookAccessToken') throw new errors.UnprocessableEntity('Invalid facebookAccessToken')
req.user.set({ facebookID, email, anonymous: false }) req.user.set({ facebookID, email, anonymous: false })
try yield module.exports.finishSignup(req, res)
yield req.user.save()
catch e
if e.code is 11000 # Duplicate key error
throw new errors.Conflict('Email already taken')
else
throw e
req.user.sendWelcomeEmail()
res.status(200).send(req.user.toObject({req: req}))
signupWithGPlus: wrap (req, res) -> signupWithGPlus: wrap (req, res) ->
unless req.user.isAnonymous() unless req.user.isAnonymous()
@ -186,6 +170,9 @@ module.exports =
throw new errors.UnprocessableEntity('Invalid gplusAccessToken') throw new errors.UnprocessableEntity('Invalid gplusAccessToken')
req.user.set({ gplusID, email, anonymous: false }) req.user.set({ gplusID, email, anonymous: false })
yield module.exports.finishSignup(req, res)
finishSignup: co.wrap (req, res) ->
try try
yield req.user.save() yield req.user.save()
catch e catch e
@ -194,5 +181,21 @@ module.exports =
else else
throw e throw e
# post-successful account signup tasks
req.user.sendWelcomeEmail() req.user.sendWelcomeEmail()
# If person A creates a trial request without creating an account, then person B uses that computer
# to create an account, then person A's trial request is associated with person B's account. To prevent
# this, we check that the signup email matches the trial request email, for every signup. If they do
# not match, the trial request applicant field is cleared, disassociating the trial request from this
# account.
trialRequest = yield TrialRequest.findOne({applicant: req.user._id})
if trialRequest
email = trialRequest.get('properties')?.email or ''
emailLower = email.toLowerCase()
if emailLower and emailLower isnt req.user.get('emailLower')
log.warn('User submitted trial request and created account with different emails. Disassociating trial request.')
yield trialRequest.update({$unset: {applicant: ''}})
res.status(200).send(req.user.toObject({req: req})) res.status(200).send(req.user.toObject({req: req}))

View file

@ -3,6 +3,7 @@ utils = require '../utils'
urlUser = '/db/user' urlUser = '/db/user'
User = require '../../../server/models/User' User = require '../../../server/models/User'
Classroom = require '../../../server/models/Classroom' Classroom = require '../../../server/models/Classroom'
TrialRequest = require '../../../server/models/TrialRequest'
Prepaid = require '../../../server/models/Prepaid' Prepaid = require '../../../server/models/Prepaid'
request = require '../request' request = require '../request'
facebook = require '../../../server/lib/facebook' facebook = require '../../../server/lib/facebook'
@ -706,6 +707,32 @@ describe 'POST /db/user/:handle/signup-with-password', ->
[res, body] = yield request.postAsync({url, json}) [res, body] = yield request.postAsync({url, json})
expect(res.statusCode).toBe(409) expect(res.statusCode).toBe(409)
done() done()
it 'disassociates the user from their trial request if the trial request email and signup email do not match', utils.wrap (done) ->
user = yield utils.becomeAnonymous()
trialRequest = yield utils.makeTrialRequest({ properties: { email: 'one@email.com' } })
expect(trialRequest.get('applicant').equals(user._id)).toBe(true)
url = getURL("/db/user/#{user.id}/signup-with-password")
email = 'two@email.com'
json = { email, password: '12345' }
[res, body] = yield request.postAsync({url, json})
expect(res.statusCode).toBe(200)
trialRequest = yield TrialRequest.findById(trialRequest.id)
expect(trialRequest.get('applicant')).toBeUndefined()
done()
it 'does NOT disassociate the user from their trial request if the trial request email and signup email DO match', utils.wrap (done) ->
user = yield utils.becomeAnonymous()
trialRequest = yield utils.makeTrialRequest({ properties: { email: 'one@email.com' } })
expect(trialRequest.get('applicant').equals(user._id)).toBe(true)
url = getURL("/db/user/#{user.id}/signup-with-password")
email = 'one@email.com'
json = { email, password: '12345' }
[res, body] = yield request.postAsync({url, json})
expect(res.statusCode).toBe(200)
trialRequest = yield TrialRequest.findById(trialRequest.id)
expect(trialRequest.get('applicant').equals(user._id)).toBe(true)
done()
describe 'POST /db/user/:handle/signup-with-facebook', -> describe 'POST /db/user/:handle/signup-with-facebook', ->