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.fixedSeed = args.fixedSeed;
self.debugWorld.flagHistory = args.flagHistory;
self.debugWorld.realTimeInputEvents = args.realTimeInputEvents;
self.debugWorld.difficulty = args.difficulty;
if (args.level)
self.debugWorld.loadFromLevel(args.level, true);
@ -381,6 +382,7 @@ self.runWorld = function runWorld(args) {
self.world.submissionCount = args.submissionCount;
self.world.fixedSeed = args.fixedSeed;
self.world.flagHistory = args.flagHistory || [];
self.world.realTimeInputEvents = args.realTimeInputEvents || [];
self.world.difficulty = args.difficulty || 0;
if(args.level)
self.world.loadFromLevel(args.level, true);
@ -539,6 +541,11 @@ self.addFlagEvent = function addFlagEvent(flagEvent) {
self.world.addFlagEvent(flagEvent);
};
self.addRealTimeInputEvent = function addRealTimeInputEvent(realTimeInputEvent) {
if(!self.world) return;
self.world.addRealTimeInputEvent(realTimeInputEvent);
};
self.stopRealTimePlayback = function stopRealTimePlayback() {
if(!self.world) return;
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
if not _.string.startsWith(e.item.id, 'vendor')
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
@recentLoadedBytes += e.rawResult.length
dependencies = @parseDependencies(e.rawResult)

View file

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

View file

@ -6,6 +6,7 @@
World = require 'lib/world/world'
CocoClass = require 'core/CocoClass'
Angel = require 'lib/Angel'
GameUIState = require 'models/GameUIState'
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']
@ -18,15 +19,17 @@ module.exports = class God extends CocoClass
constructor: (options) ->
options ?= {}
@retrieveValueFromFrame = _.throttle @retrieveValueFromFrame, 1000
@gameUIState ?= options.gameUIState or new GameUIState()
super()
# Angels are all given access to this.
@angelsShare =
@angelsShare = {
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
spectate: options.spectate
god: @
godNick: @nick
@gameUIState
workQueue: []
firstWorld: true
world: undefined
@ -34,6 +37,7 @@ module.exports = class God extends CocoClass
worldClassMap: undefined
angels: []
busyAngels: [] # Busy angels will automatically register here.
}
# Determine how many concurrent Angels/web workers to use at a time
# ~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({
canDragCamera: true
})
@realTimeInputEvents = @gameUIState.get('realTimeInputEvents')
@listenTo(@gameUIState, 'sprite:mouse-down', @onSpriteMouseDown)
@initEasel()
@initAudio()
@onResize = _.debounce @onResize, resizeDelay
@ -510,6 +512,15 @@ module.exports = Surface = class Surface extends CocoClass
@gameUIState.trigger('surface:stage-mouse-down', event)
@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) =>
return if @disabled
onBackground = not @webGLStage.hitTest e.stageX, e.stageY
@ -596,6 +607,7 @@ module.exports = Surface = class Surface extends CocoClass
onRealTimePlaybackStarted: (e) ->
return if @realTime
@realTimeInputEvents.reset()
@realTime = true
@onResize()
@playing = false # Will start when countdown is done.

View file

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

View file

@ -26,4 +26,5 @@ module.exports = class GameUIState extends CocoModel
defaults: -> {
selected: []
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}
state = session.get 'state'
state.flagHistory = session.solution.flagHistory
state.realTimeInputEvents = session.solution.realTimeInputEvents
state.difficulty = session.solution.difficulty or 0
session.solution.seed = undefined unless _.isNumber session.solution.seed # TODO: migrate away from submissionCount/sessionID seed objects
catch e

View file

@ -20,6 +20,7 @@ Article = require 'models/Article'
Camera = require 'lib/surface/Camera'
AudioPlayer = require 'lib/AudioPlayer'
Simulator = require 'lib/simulator/Simulator'
GameUIState = require 'models/GameUIState'
# subviews
LevelLoadingView = require './LevelLoadingView'
@ -108,6 +109,7 @@ module.exports = class PlayLevelView extends RootView
@opponentSessionID = @getQueryVariable('opponent')
@opponentSessionID ?= @options.opponent
@gameUIState = new GameUIState()
$(window).on 'resize', @onWindowResize
@saveScreenshot = _.throttle @saveScreenshot, 30000
@ -137,7 +139,7 @@ module.exports = class PlayLevelView extends RootView
load: ->
@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
if me.isSessionless()
levelLoaderOptions.fakeSessionConfig = {}
@ -345,12 +347,14 @@ module.exports = class PlayLevelView extends RootView
initSurface: ->
webGLSurface = $('canvas#webgl-surface', @$el)
normalSurface = $('canvas#normal-surface', @$el)
surfaceOptions =
surfaceOptions = {
thangTypes: @supermodel.getModels(ThangType)
observing: @observing
@observing
playerNames: @findPlayerNames()
levelType: @level.get('type', true)
stayVisible: @showAds()
@gameUIState
}
@surface = new Surface(@world, normalSurface, webGLSurface, surfaceOptions)
worldBounds = @world.getBounds()
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
unless spell?.canRead()
@clearSpellView()
@updateSpellPalette thang, spell
@updateSpellPalette thang, spell if spell
return
unless spell.view is @spellView
@clearSpellView()

View file

@ -13,6 +13,8 @@ User = require '../models/User'
Classroom = require '../models/Classroom'
facebook = require '../lib/facebook'
gplus = require '../lib/gplus'
TrialRequest = require '../models/TrialRequest'
log = require 'winston'
module.exports =
fetchByGPlusID: wrap (req, res, next) ->
@ -133,16 +135,7 @@ module.exports =
throw new errors.Conflict('Email already taken')
req.user.set({ password, email, anonymous: false })
try
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}))
yield module.exports.finishSignup(req, res)
signupWithFacebook: wrap (req, res) ->
unless req.user.isAnonymous()
@ -159,16 +152,7 @@ module.exports =
throw new errors.UnprocessableEntity('Invalid facebookAccessToken')
req.user.set({ facebookID, email, anonymous: false })
try
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}))
yield module.exports.finishSignup(req, res)
signupWithGPlus: wrap (req, res) ->
unless req.user.isAnonymous()
@ -186,6 +170,9 @@ module.exports =
throw new errors.UnprocessableEntity('Invalid gplusAccessToken')
req.user.set({ gplusID, email, anonymous: false })
yield module.exports.finishSignup(req, res)
finishSignup: co.wrap (req, res) ->
try
yield req.user.save()
catch e
@ -194,5 +181,21 @@ module.exports =
else
throw e
# post-successful account signup tasks
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}))

View file

@ -3,6 +3,7 @@ utils = require '../utils'
urlUser = '/db/user'
User = require '../../../server/models/User'
Classroom = require '../../../server/models/Classroom'
TrialRequest = require '../../../server/models/TrialRequest'
Prepaid = require '../../../server/models/Prepaid'
request = require '../request'
facebook = require '../../../server/lib/facebook'
@ -706,6 +707,32 @@ describe 'POST /db/user/:handle/signup-with-password', ->
[res, body] = yield request.postAsync({url, json})
expect(res.statusCode).toBe(409)
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', ->