Merge branch 'master' into production

This commit is contained in:
Nick Winter 2016-06-29 12:43:24 -07:00
commit 90d5e9ddce
22 changed files with 859 additions and 532 deletions

View file

@ -428,17 +428,18 @@ self.onWorldLoaded = function onWorldLoaded() {
if(self.world.framesSerializedSoFar == self.world.frames.length) return; if(self.world.framesSerializedSoFar == self.world.frames.length) return;
if(self.world.ended) if(self.world.ended)
self.goalManager.worldGenerationEnded(); self.goalManager.worldGenerationEnded();
var goalStates = self.goalManager.getGoalStates();
var overallStatus = self.goalManager.checkOverallStatus();
var totalFrames = self.world.totalFrames;
if(self.world.ended) {
var lastFrameHash = self.world.frames[totalFrames - 2].hash
self.postMessage({type: 'end-load-frames', goalStates: goalStates, overallStatus: overallStatus, totalFrames: totalFrames, lastFrameHash: lastFrameHash});
}
var t1 = new Date(); var t1 = new Date();
var diff = t1 - self.t0; var diff = t1 - self.t0;
if(self.world.headless) var goalStates = self.goalManager.getGoalStates();
return console.log('Headless simulation completed in ' + diff + 'ms.'); var totalFrames = self.world.totalFrames;
if(self.world.ended) {
var overallStatus = self.goalManager.checkOverallStatus();
var lastFrameHash = self.world.frames[totalFrames - 2].hash
var simulationFrameRate = self.world.frames.length / diff * 1000 * 30 / self.world.frameRate
self.postMessage({type: 'end-load-frames', goalStates: goalStates, overallStatus: overallStatus, totalFrames: totalFrames, lastFrameHash: lastFrameHash, simulationFrameRate: simulationFrameRate});
if(self.world.headless)
return console.log('Headless simulation completed in ' + diff + 'ms, ' + simulationFrameRate.toFixed(1) + ' FPS.');
}
var worldEnded = self.world.ended; var worldEnded = self.world.ended;
var serialized; var serialized;
@ -469,7 +470,7 @@ self.onWorldLoaded = function onWorldLoaded() {
if(worldEnded) { if(worldEnded) {
var t3 = new Date(); var t3 = new Date();
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"); 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\nFPS :", simulationFrameRate.toFixed(1));
} }
}; };
@ -483,7 +484,10 @@ self.onWorldPreloaded = function onWorldPreloaded() {
self.goalManager.worldGenerationEnded(); self.goalManager.worldGenerationEnded();
var goalStates = self.goalManager.getGoalStates(); var goalStates = self.goalManager.getGoalStates();
var overallStatus = self.goalManager.checkOverallStatus(); var overallStatus = self.goalManager.checkOverallStatus();
self.postMessage({type: 'end-preload-frames', goalStates: goalStates, overallStatus: overallStatus}); var t1 = new Date();
var diff = t1 - self.t0;
var simulationFrameRate = self.world.frames.length / diff * 1000 * 30 / self.world.frameRate
self.postMessage({type: 'end-preload-frames', goalStates: goalStates, overallStatus: overallStatus, simulationFrameRate: simulationFrameRate});
}; };
self.onWorldError = function onWorldError(error) { self.onWorldError = function onWorldError(error) {

View file

@ -370,3 +370,103 @@ module.exports.findNextLevel = (levels, currentIndex, needsPractice) ->
module.exports.needsPractice = (playtime=0, threshold=2) -> module.exports.needsPractice = (playtime=0, threshold=2) ->
playtime / 60 > threshold playtime / 60 > threshold
module.exports.usStateCodes =
# https://github.com/mdzhang/us-state-codes
# generated by js2coffee 2.2.0
(->
stateNamesByCode =
'AL': 'Alabama'
'AK': 'Alaska'
'AZ': 'Arizona'
'AR': 'Arkansas'
'CA': 'California'
'CO': 'Colorado'
'CT': 'Connecticut'
'DE': 'Delaware'
'DC': 'District of Columbia'
'FL': 'Florida'
'GA': 'Georgia'
'HI': 'Hawaii'
'ID': 'Idaho'
'IL': 'Illinois'
'IN': 'Indiana'
'IA': 'Iowa'
'KS': 'Kansas'
'KY': 'Kentucky'
'LA': 'Louisiana'
'ME': 'Maine'
'MD': 'Maryland'
'MA': 'Massachusetts'
'MI': 'Michigan'
'MN': 'Minnesota'
'MS': 'Mississippi'
'MO': 'Missouri'
'MT': 'Montana'
'NE': 'Nebraska'
'NV': 'Nevada'
'NH': 'New Hampshire'
'NJ': 'New Jersey'
'NM': 'New Mexico'
'NY': 'New York'
'NC': 'North Carolina'
'ND': 'North Dakota'
'OH': 'Ohio'
'OK': 'Oklahoma'
'OR': 'Oregon'
'PA': 'Pennsylvania'
'RI': 'Rhode Island'
'SC': 'South Carolina'
'SD': 'South Dakota'
'TN': 'Tennessee'
'TX': 'Texas'
'UT': 'Utah'
'VT': 'Vermont'
'VA': 'Virginia'
'WA': 'Washington'
'WV': 'West Virginia'
'WI': 'Wisconsin'
'WY': 'Wyoming'
stateCodesByName = _.invert(stateNamesByCode)
# normalizes case and removes invalid characters
# returns null if can't find sanitized code in the state map
sanitizeStateCode = (code) ->
code = if _.isString(code) then code.trim().toUpperCase().replace(/[^A-Z]/g, '') else null
if stateNamesByCode[code] then code else null
# returns a valid state name else null
getStateNameByStateCode = (code) ->
stateNamesByCode[sanitizeStateCode(code)] or null
# normalizes case and removes invalid characters
# returns null if can't find sanitized name in the state map
sanitizeStateName = (name) ->
if !_.isString(name)
return null
# bad whitespace remains bad whitespace e.g. "O hi o" is not valid
name = name.trim().toLowerCase().replace(/[^a-z\s]/g, '').replace(/\s+/g, ' ')
tokens = name.split(/\s+/)
tokens = _.map(tokens, (token) ->
token.charAt(0).toUpperCase() + token.slice(1)
)
# account for District of Columbia
if tokens.length > 2
tokens[1] = tokens[1].toLowerCase()
name = tokens.join(' ')
if stateCodesByName[name] then name else null
# returns a valid state code else null
getStateCodeByStateName = (name) ->
stateCodesByName[sanitizeStateName(name)] or null
return {
sanitizeStateCode: sanitizeStateCode
getStateNameByStateCode: getStateNameByStateCode
sanitizeStateName: sanitizeStateName
getStateCodeByStateName: getStateCodeByStateName
}
)()

View file

@ -82,11 +82,10 @@ module.exports = class Angel extends CocoClass
clearTimeout @condemnTimeout clearTimeout @condemnTimeout
when 'end-load-frames' when 'end-load-frames'
clearTimeout @condemnTimeout clearTimeout @condemnTimeout
@beholdGoalStates event.data.goalStates, event.data.overallStatus, false, event.data.totalFrames, event.data.lastFrameHash # Work ends here if we're headless. @beholdGoalStates {goalStates: event.data.goalStates, overallStatus: event.data.overallStatus, preload: false, totalFrames: event.data.totalFrames, lastFrameHash: event.data.lastFrameHash, simulationFrameRate: event.data.simulationFrameRate} # Work ends here if we're headless.
when 'end-preload-frames' when 'end-preload-frames'
clearTimeout @condemnTimeout clearTimeout @condemnTimeout
@beholdGoalStates event.data.goalStates, event.data.overallStatus, true @beholdGoalStates {goalStates: event.data.goalStates, overallStatus: event.data.overallStatus, preload: true, simulationFrameRate: event.data.simulationFrameRate}
# We have to abort like an infinite loop if we see one of these; they're not really recoverable # We have to abort like an infinite loop if we see one of these; they're not really recoverable
when 'non-user-code-problem' when 'non-user-code-problem'
@ -125,11 +124,12 @@ module.exports = class Angel extends CocoClass
else else
@log 'Received unsupported message:', event.data @log 'Received unsupported message:', event.data
beholdGoalStates: (goalStates, overallStatus, preload=false, totalFrames=undefined, lastFrameHash=undefined) -> beholdGoalStates: ({goalStates, overallStatus, preload, totalFrames, lastFrameHash, simulationFrameRate}) ->
return if @aborting return if @aborting
event = goalStates: goalStates, preload: preload, overallStatus: overallStatus event = goalStates: goalStates, preload: preload ? false, overallStatus: overallStatus
event.totalFrames = totalFrames if totalFrames? event.totalFrames = totalFrames if totalFrames?
event.lastFrameHash = lastFrameHash if lastFrameHash? event.lastFrameHash = lastFrameHash if lastFrameHash?
event.simulationFrameRate = simulationFrameRate if simulationFrameRate?
@publishGodEvent 'goals-calculated', event @publishGodEvent 'goals-calculated', event
@finishWork() if @shared.headless @finishWork() if @shared.headless
@ -306,7 +306,8 @@ module.exports = class Angel extends CocoClass
work.world.goalManager.worldGenerationEnded() if work.world.ended work.world.goalManager.worldGenerationEnded() if work.world.ended
if work.headless if work.headless
@beholdGoalStates goalStates, testGM.checkOverallStatus(), false, work.world.totalFrames, work.world.frames[work.world.totalFrames - 2]?.hash simulationFrameRate = work.world.frames.length / (work.t2 - work.t1) * 1000 * 30 / work.world.frameRate
@beholdGoalStates {goalStates, overallStatus: testGM.checkOverallStatus(), preload: false, totalFrames: work.world.totalFrames, lastFrameHash: work.world.frames[work.world.totalFrames - 2]?.hash, simulationFrameRate: simulationFrameRate}
return return
serialized = world.serialize() serialized = world.serialize()

View file

@ -1,4 +1,5 @@
CocoClass = require 'core/CocoClass' CocoClass = require 'core/CocoClass'
GameUIState = require 'models/GameUIState'
# If I were the kind of math major who remembered his math, this would all be done with matrix transforms. # If I were the kind of math major who remembered his math, this would all be done with matrix transforms.
@ -44,12 +45,17 @@ module.exports = class Camera extends CocoClass
'camera:zoom-out': 'onZoomOut' 'camera:zoom-out': 'onZoomOut'
'camera:zoom-to': 'onZoomTo' 'camera:zoom-to': 'onZoomTo'
'level:restarted': 'onLevelRestarted' 'level:restarted': 'onLevelRestarted'
'surface:mouse-scrolled': 'onMouseScrolled'
'sprite:mouse-down': 'onMouseDown'
'sprite:dragged': 'onMouseDragged'
constructor: (@canvas, angle=Math.asin(0.75), hFOV=d2r(30)) -> constructor: (@canvas, @options={}) ->
angle=Math.asin(0.75)
hFOV=d2r(30)
super() super()
@gameUIState = @options.gameUIState or new GameUIState()
@listenTo @gameUIState, 'surface:stage-mouse-move', @onMouseMove
@listenTo @gameUIState, 'surface:stage-mouse-down', @onMouseDown
@listenTo @gameUIState, 'surface:stage-mouse-up', @onMouseUp
@listenTo @gameUIState, 'surface:mouse-scrolled', @onMouseScrolled
@handleEvents = @options.handleEvents ? true
@canvasWidth = parseInt(@canvas.attr('width'), 10) @canvasWidth = parseInt(@canvas.attr('width'), 10)
@canvasHeight = parseInt(@canvas.attr('height'), 10) @canvasHeight = parseInt(@canvas.attr('height'), 10)
@offset = {x: 0, y: 0} @offset = {x: 0, y: 0}
@ -155,8 +161,27 @@ module.exports = class Camera extends CocoClass
onZoomIn: (e) -> @zoomTo @target, @zoom * 1.15, 300 onZoomIn: (e) -> @zoomTo @target, @zoom * 1.15, 300
onZoomOut: (e) -> @zoomTo @target, @zoom / 1.15, 300 onZoomOut: (e) -> @zoomTo @target, @zoom / 1.15, 300
onMouseDown: (e) ->
return if @dragDisabled
@lastPos = {x: e.originalEvent.rawX, y: e.originalEvent.rawY}
@mousePressed = true
onMouseMove: (e) ->
return unless @mousePressed and @gameUIState.get('canDragCamera')
return if @dragDisabled
target = @boundTarget(@target, @zoom)
newPos =
x: target.x + (@lastPos.x - e.originalEvent.rawX) / @zoom
y: target.y + (@lastPos.y - e.originalEvent.rawY) / @zoom
@zoomTo newPos, @zoom, 0
@lastPos = {x: e.originalEvent.rawX, y: e.originalEvent.rawY}
Backbone.Mediator.publish 'camera:dragged', {}
onMouseUp: (e) ->
@mousePressed = false
onMouseScrolled: (e) -> onMouseScrolled: (e) ->
return unless e.canvas is @canvas
ratio = 1 + 0.05 * Math.sqrt(Math.abs(e.deltaY)) ratio = 1 + 0.05 * Math.sqrt(Math.abs(e.deltaY))
ratio = 1 / ratio if e.deltaY > 0 ratio = 1 / ratio if e.deltaY > 0
newZoom = @zoom * ratio newZoom = @zoom * ratio
@ -174,22 +199,6 @@ module.exports = class Camera extends CocoClass
target = @target target = @target
@zoomTo target, newZoom, 0 @zoomTo target, newZoom, 0
onMouseDown: (e) ->
return unless e.canvas is @canvas[0]
return if @dragDisabled
@lastPos = {x: e.originalEvent.rawX, y: e.originalEvent.rawY}
onMouseDragged: (e) ->
return unless e.canvas is @canvas[0]
return if @dragDisabled
target = @boundTarget(@target, @zoom)
newPos =
x: target.x + (@lastPos.x - e.originalEvent.rawX) / @zoom
y: target.y + (@lastPos.y - e.originalEvent.rawY) / @zoom
@zoomTo newPos, @zoom, 0
@lastPos = {x: e.originalEvent.rawX, y: e.originalEvent.rawY}
Backbone.Mediator.publish 'camera:dragged', {}
onLevelRestarted: -> onLevelRestarted: ->
@setBounds(@firstBounds, false) @setBounds(@firstBounds, false)

View file

@ -58,11 +58,13 @@ module.exports = Lank = class Lank extends CocoClass
'surface:ticked': 'onSurfaceTicked' 'surface:ticked': 'onSurfaceTicked'
'sprite:move': 'onMove' 'sprite:move': 'onMove'
constructor: (@thangType, options) -> constructor: (@thangType, options={}) ->
super() super()
spriteName = @thangType.get('name') spriteName = @thangType.get('name')
@isMissile = /(Missile|Arrow|Spear|Bolt)/.test(spriteName) and not /(Tower|Charge)/.test(spriteName) @isMissile = /(Missile|Arrow|Spear|Bolt)/.test(spriteName) and not /(Tower|Charge)/.test(spriteName)
@options = _.extend($.extend(true, {}, @options), options) @options = _.extend($.extend(true, {}, @options), options)
@gameUIState = @options.gameUIState
@handleEvents = @options.handleEvents
@setThang @options.thang @setThang @options.thang
if @thang? if @thang?
options = @thang?.getLankOptions?() options = @thang?.getLankOptions?()
@ -496,6 +498,7 @@ module.exports = Lank = class Lank extends CocoClass
newEvent = sprite: @, thang: @thang, originalEvent: e, canvas: p.canvas newEvent = sprite: @, thang: @thang, originalEvent: e, canvas: p.canvas
@trigger ourEventName, newEvent @trigger ourEventName, newEvent
Backbone.Mediator.publish ourEventName, newEvent Backbone.Mediator.publish ourEventName, newEvent
@gameUIState.trigger(ourEventName, newEvent)
addHealthBar: -> addHealthBar: ->
return unless @thang?.health? and 'health' in (@thang?.hudProperties ? []) and @options.floatingLayer return unless @thang?.health? and 'health' in (@thang?.hudProperties ? []) and @options.floatingLayer
@ -646,6 +649,10 @@ module.exports = Lank = class Lank extends CocoClass
@marks[name] ?= new Mark name: name, lank: @, camera: @options.camera, layer: layer ? @options.groundLayer, thangType: thangType @marks[name] ?= new Mark name: name, lank: @, camera: @options.camera, layer: layer ? @options.groundLayer, thangType: thangType
@marks[name] @marks[name]
removeMark: (name) ->
@marks[name].destroy()
delete @marks[name]
notifySpeechUpdated: (e) -> notifySpeechUpdated: (e) ->
e = _.clone(e) e = _.clone(e)
e.sprite = @ e.sprite = @

View file

@ -25,10 +25,11 @@ module.exports = class LankBoss extends CocoClass
'surface:flag-appeared': 'onFlagAppeared' 'surface:flag-appeared': 'onFlagAppeared'
'surface:remove-selected-flag': 'onRemoveSelectedFlag' 'surface:remove-selected-flag': 'onRemoveSelectedFlag'
constructor: (@options) -> constructor: (@options={}) ->
super() super()
@handleEvents = @options.handleEvents
@gameUIState = @options.gameUIState
@dragged = 0 @dragged = 0
@options ?= {}
@camera = @options.camera @camera = @options.camera
@webGLStage = @options.webGLStage @webGLStage = @options.webGLStage
@surfaceTextLayer = @options.surfaceTextLayer @surfaceTextLayer = @options.surfaceTextLayer
@ -38,6 +39,8 @@ module.exports = class LankBoss extends CocoClass
@lankArray = [] # Mirror @lanks, but faster for when we just need to iterate @lankArray = [] # Mirror @lanks, but faster for when we just need to iterate
@createLayers() @createLayers()
@pendingFlags = [] @pendingFlags = []
if not @handleEvents
@listenTo @gameUIState, 'change:selected', @onChangeSelected
destroy: -> destroy: ->
@removeLank lank for thangID, lank of @lanks @removeLank lank for thangID, lank of @lanks
@ -93,7 +96,16 @@ module.exports = class LankBoss extends CocoClass
@selectionMark = new Mark name: 'selection', camera: @camera, layer: @layerAdapters['Ground'], thangType: 'selection' @selectionMark = new Mark name: 'selection', camera: @camera, layer: @layerAdapters['Ground'], thangType: 'selection'
createLankOptions: (options) -> createLankOptions: (options) ->
_.extend options, camera: @camera, resolutionFactor: SPRITE_RESOLUTION_FACTOR, groundLayer: @layerAdapters['Ground'], textLayer: @surfaceTextLayer, floatingLayer: @layerAdapters['Floating'], showInvisible: @options.showInvisible _.extend options, {
@camera
resolutionFactor: SPRITE_RESOLUTION_FACTOR
groundLayer: @layerAdapters['Ground']
textLayer: @surfaceTextLayer
floatingLayer: @layerAdapters['Floating']
showInvisible: @options.showInvisible
@gameUIState
@handleEvents
}
onSetDebug: (e) -> onSetDebug: (e) ->
return if e.debug is @debug return if e.debug is @debug
@ -256,6 +268,7 @@ module.exports = class LankBoss extends CocoClass
@dragged += 1 @dragged += 1
onLankMouseUp: (e) -> onLankMouseUp: (e) ->
return unless @handleEvents
return if key.shift #and @options.choosing return if key.shift #and @options.choosing
return @dragged = 0 if @dragged > 3 return @dragged = 0 if @dragged > 3
@dragged = 0 @dragged = 0
@ -264,9 +277,27 @@ module.exports = class LankBoss extends CocoClass
@selectLank e, lank @selectLank e, lank
onStageMouseDown: (e) -> onStageMouseDown: (e) ->
return unless @handleEvents
return if key.shift #and @options.choosing return if key.shift #and @options.choosing
@selectLank e if e.onBackground @selectLank e if e.onBackground
onChangeSelected: (gameUIState, selected) ->
oldLanks = (s.sprite for s in gameUIState.previousAttributes().selected or [])
newLanks = (s.sprite for s in selected or [])
addedLanks = _.difference(newLanks, oldLanks)
removedLanks = _.difference(oldLanks, newLanks)
for lank in addedLanks
layer = if lank.sprite.parent isnt @layerAdapters.Default.container then @layerAdapters.Default else @layerAdapters.Ground
mark = new Mark name: 'selection', camera: @camera, layer: layer, thangType: 'selection'
mark.toggle true
mark.setLank(lank)
mark.update()
lank.marks.selection = mark # TODO: Figure out how to non-hackily assign lank this mark
for lank in removedLanks
lank.removeMark?('selection')
selectThang: (thangID, spellName=null, treemaThangSelected = null) -> selectThang: (thangID, spellName=null, treemaThangSelected = null) ->
return @willSelectThang = [thangID, spellName] unless @lanks[thangID] return @willSelectThang = [thangID, spellName] unless @lanks[thangID]
@selectLank null, @lanks[thangID], spellName, treemaThangSelected @selectLank null, @lanks[thangID], spellName, treemaThangSelected
@ -275,8 +306,9 @@ module.exports = class LankBoss extends CocoClass
return if e and (@disabled or @selectLocked) # Ignore clicks for selection/panning/wizard movement while disabled or select is locked return if e and (@disabled or @selectLocked) # Ignore clicks for selection/panning/wizard movement while disabled or select is locked
worldPos = lank?.thang?.pos worldPos = lank?.thang?.pos
worldPos ?= @camera.screenToWorld {x: e.originalEvent.rawX, y: e.originalEvent.rawY} if e?.originalEvent worldPos ?= @camera.screenToWorld {x: e.originalEvent.rawX, y: e.originalEvent.rawY} if e?.originalEvent
if (not @reallyStopMoving) and worldPos and (@options.navigateToSelection or not lank or treemaThangSelected) and e?.originalEvent?.nativeEvent?.which isnt 3 if @handleEvents
@camera.zoomTo(lank?.sprite or @camera.worldToSurface(worldPos), @camera.zoom, 1000, true) if (not @reallyStopMoving) and worldPos and (@options.navigateToSelection or not lank or treemaThangSelected) and e?.originalEvent?.nativeEvent?.which isnt 3
@camera.zoomTo(lank?.sprite or @camera.worldToSurface(worldPos), @camera.zoom, 1000, true)
lank = null if @options.choosing # Don't select lanks while choosing lank = null if @options.choosing # Don't select lanks while choosing
if lank isnt @selectedLank if lank isnt @selectedLank
@selectedLank?.selected = false @selectedLank?.selected = false

View file

@ -18,6 +18,7 @@ LankBoss = require './LankBoss'
PointChooser = require './PointChooser' PointChooser = require './PointChooser'
RegionChooser = require './RegionChooser' RegionChooser = require './RegionChooser'
MusicPlayer = require './MusicPlayer' MusicPlayer = require './MusicPlayer'
GameUIState = require 'models/GameUIState'
resizeDelay = 500 # At least as much as $level-resize-transition-time. resizeDelay = 500 # At least as much as $level-resize-transition-time.
@ -87,6 +88,10 @@ module.exports = Surface = class Surface extends CocoClass
@normalLayers = [] @normalLayers = []
@options = _.clone(@defaults) @options = _.clone(@defaults)
@options = _.extend(@options, givenOptions) if givenOptions @options = _.extend(@options, givenOptions) if givenOptions
@handleEvents = @options.handleEvents ? true
@gameUIState = @options.gameUIState or new GameUIState({
canDragCamera: true
})
@initEasel() @initEasel()
@initAudio() @initAudio()
@onResize = _.debounce @onResize, resizeDelay @onResize = _.debounce @onResize, resizeDelay
@ -98,7 +103,7 @@ module.exports = Surface = class Surface extends CocoClass
@normalStage = new createjs.Stage(@normalCanvas[0]) @normalStage = new createjs.Stage(@normalCanvas[0])
@webGLStage = new createjs.SpriteStage(@webGLCanvas[0]) @webGLStage = new createjs.SpriteStage(@webGLCanvas[0])
@normalStage.nextStage = @webGLStage @normalStage.nextStage = @webGLStage
@camera = new Camera @webGLCanvas @camera = new Camera(@webGLCanvas, { @gameUIState, @handleEvents })
AudioPlayer.camera = @camera unless @options.choosing AudioPlayer.camera = @camera unless @options.choosing
@normalLayers.push @surfaceTextLayer = new Layer name: 'Surface Text', layerPriority: 1, transform: Layer.TRANSFORM_SURFACE_TEXT, camera: @camera @normalLayers.push @surfaceTextLayer = new Layer name: 'Surface Text', layerPriority: 1, transform: Layer.TRANSFORM_SURFACE_TEXT, camera: @camera
@ -112,7 +117,19 @@ module.exports = Surface = class Surface extends CocoClass
canvasHeight = parseInt @normalCanvas.attr('height'), 10 canvasHeight = parseInt @normalCanvas.attr('height'), 10
@screenLayer.addChild new Letterbox canvasWidth: canvasWidth, canvasHeight: canvasHeight @screenLayer.addChild new Letterbox canvasWidth: canvasWidth, canvasHeight: canvasHeight
@lankBoss = new LankBoss camera: @camera, webGLStage: @webGLStage, surfaceTextLayer: @surfaceTextLayer, world: @world, thangTypes: @options.thangTypes, choosing: @options.choosing, navigateToSelection: @options.navigateToSelection, showInvisible: @options.showInvisible, playerNames: if @options.levelType is 'course-ladder' then @options.playerNames else null @lankBoss = new LankBoss({
@camera
@webGLStage
@surfaceTextLayer
@world
thangTypes: @options.thangTypes
choosing: @options.choosing
navigateToSelection: @options.navigateToSelection
showInvisible: @options.showInvisible
playerNames: if @options.levelType is 'course-ladder' then @options.playerNames else null
@gameUIState
@handleEvents
})
@countdownScreen = new CountdownScreen camera: @camera, layer: @screenLayer, showsCountdown: @world.showsCountdown @countdownScreen = new CountdownScreen camera: @camera, layer: @screenLayer, showsCountdown: @world.showsCountdown
@playbackOverScreen = new PlaybackOverScreen camera: @camera, layer: @screenLayer, playerNames: @options.playerNames @playbackOverScreen = new PlaybackOverScreen camera: @camera, layer: @screenLayer, playerNames: @options.playerNames
@normalStage.addChildAt @playbackOverScreen.dimLayer, 0 # Put this below the other layers, actually, so we can more easily read text on the screen. @normalStage.addChildAt @playbackOverScreen.dimLayer, 0 # Put this below the other layers, actually, so we can more easily read text on the screen.
@ -121,7 +138,7 @@ module.exports = Surface = class Surface extends CocoClass
@webGLStage.enableMouseOver(10) @webGLStage.enableMouseOver(10)
@webGLStage.addEventListener 'stagemousemove', @onMouseMove @webGLStage.addEventListener 'stagemousemove', @onMouseMove
@webGLStage.addEventListener 'stagemousedown', @onMouseDown @webGLStage.addEventListener 'stagemousedown', @onMouseDown
@webGLCanvas[0].addEventListener 'mouseup', @onMouseUp @webGLStage.addEventListener 'stagemouseup', @onMouseUp
@webGLCanvas.on 'mousewheel', @onMouseWheel @webGLCanvas.on 'mousewheel', @onMouseWheel
@hookUpChooseControls() if @options.choosing # TODO: figure this stuff out @hookUpChooseControls() if @options.choosing # TODO: figure this stuff out
createjs.Ticker.timingMode = createjs.Ticker.RAF_SYNCHED createjs.Ticker.timingMode = createjs.Ticker.RAF_SYNCHED
@ -222,8 +239,9 @@ module.exports = Surface = class Surface extends CocoClass
updateState: (frameChanged) -> updateState: (frameChanged) ->
# world state must have been restored in @restoreWorldState # world state must have been restored in @restoreWorldState
if @playing and @currentFrame < @world.frames.length - 1 and @heroLank and not @mouseIsDown and @camera.newTarget isnt @heroLank.sprite and @camera.target isnt @heroLank.sprite if @handleEvents
@camera.zoomTo @heroLank.sprite, @camera.zoom, 750 if @playing and @currentFrame < @world.frames.length - 1 and @heroLank and not @mouseIsDown and @camera.newTarget isnt @heroLank.sprite and @camera.target isnt @heroLank.sprite
@camera.zoomTo @heroLank.sprite, @camera.zoom, 750
@lankBoss.update frameChanged @lankBoss.update frameChanged
@camera.updateZoom() # Make sure to do this right after the LankBoss updates, not before, so it can properly target sprite positions. @camera.updateZoom() # Make sure to do this right after the LankBoss updates, not before, so it can properly target sprite positions.
@dimmer?.setSprites @lankBoss.lanks @dimmer?.setSprites @lankBoss.lanks
@ -371,7 +389,8 @@ module.exports = Surface = class Surface extends CocoClass
target = null target = null
@camera.setBounds e.bounds if e.bounds @camera.setBounds e.bounds if e.bounds
# @cameraBorder.updateBounds @camera.bounds # @cameraBorder.updateBounds @camera.bounds
@camera.zoomTo target, e.zoom, e.duration # TODO: SurfaceScriptModule perhaps shouldn't assign e.zoom if not set if @handleEvents
@camera.zoomTo target, e.zoom, e.duration # TODO: SurfaceScriptModule perhaps shouldn't assign e.zoom if not set
onZoomUpdated: (e) -> onZoomUpdated: (e) ->
if @ended if @ended
@ -476,6 +495,7 @@ module.exports = Surface = class Surface extends CocoClass
@mouseScreenPos = {x: e.stageX, y: e.stageY} @mouseScreenPos = {x: e.stageX, y: e.stageY}
return if @disabled return if @disabled
Backbone.Mediator.publish 'surface:mouse-moved', x: e.stageX, y: e.stageY Backbone.Mediator.publish 'surface:mouse-moved', x: e.stageX, y: e.stageY
@gameUIState.trigger('surface:stage-mouse-move', { originalEvent: e })
onMouseDown: (e) => onMouseDown: (e) =>
return if @disabled return if @disabled
@ -484,16 +504,19 @@ module.exports = Surface = class Surface extends CocoClass
onBackground = not @webGLStage._getObjectsUnderPoint(e.stageX, e.stageY, null, true) onBackground = not @webGLStage._getObjectsUnderPoint(e.stageX, e.stageY, null, true)
wop = @camera.screenToWorld x: e.stageX, y: e.stageY wop = @camera.screenToWorld x: e.stageX, y: e.stageY
event = onBackground: onBackground, x: e.stageX, y: e.stageY, originalEvent: e, worldPos: wop event = { onBackground: onBackground, x: e.stageX, y: e.stageY, originalEvent: e, worldPos: wop }
Backbone.Mediator.publish 'surface:stage-mouse-down', event Backbone.Mediator.publish 'surface:stage-mouse-down', event
Backbone.Mediator.publish 'tome:focus-editor', {} Backbone.Mediator.publish 'tome:focus-editor', {}
@gameUIState.trigger('surface:stage-mouse-down', event)
@mouseIsDown = true @mouseIsDown = true
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
Backbone.Mediator.publish 'surface:stage-mouse-up', onBackground: onBackground, x: e.stageX, y: e.stageY, originalEvent: e event = { onBackground: onBackground, x: e.stageX, y: e.stageY, originalEvent: e }
Backbone.Mediator.publish 'surface:stage-mouse-up', event
Backbone.Mediator.publish 'tome:focus-editor', {} Backbone.Mediator.publish 'tome:focus-editor', {}
@gameUIState.trigger('surface:stage-mouse-up', event)
@mouseIsDown = false @mouseIsDown = false
onMouseWheel: (e) => onMouseWheel: (e) =>
@ -506,6 +529,7 @@ module.exports = Surface = class Surface extends CocoClass
canvas: @webGLCanvas canvas: @webGLCanvas
event.screenPos = @mouseScreenPos if @mouseScreenPos event.screenPos = @mouseScreenPos if @mouseScreenPos
Backbone.Mediator.publish 'surface:mouse-scrolled', event unless @disabled Backbone.Mediator.publish 'surface:mouse-scrolled', event unless @disabled
@gameUIState.trigger('surface:mouse-scrolled', event)
#- Canvas callbacks #- Canvas callbacks
@ -585,8 +609,9 @@ module.exports = Surface = class Surface extends CocoClass
@onResize() @onResize()
_.delay @onResize, resizeDelay + 100 # Do it again just to be double sure that we don't stay zoomed in due to timing problems. _.delay @onResize, resizeDelay + 100 # Do it again just to be double sure that we don't stay zoomed in due to timing problems.
@normalCanvas.add(@webGLCanvas).removeClass 'flag-color-selected' @normalCanvas.add(@webGLCanvas).removeClass 'flag-color-selected'
if @previousCameraZoom if @handleEvents
@camera.zoomTo @camera.newTarget or @camera.target, @previousCameraZoom, 3000 if @previousCameraZoom
@camera.zoomTo @camera.newTarget or @camera.target, @previousCameraZoom, 3000
onFlagColorSelected: (e) -> onFlagColorSelected: (e) ->
@normalCanvas.add(@webGLCanvas).toggleClass 'flag-color-selected', Boolean(e.color) @normalCanvas.add(@webGLCanvas).toggleClass 'flag-color-selected', Boolean(e.color)

File diff suppressed because it is too large Load diff

View file

@ -6,7 +6,7 @@ module.exports = nativeDescription: "Nederlands (Nederland)", englishDescription
play: "Speel" # The big play button that opens up the campaign view. play: "Speel" # The big play button that opens up the campaign view.
play_campaign_version: "Speel de Verhaallijn" # Shows up under big play button if you only play /courses play_campaign_version: "Speel de Verhaallijn" # Shows up under big play button if you only play /courses
old_browser: "uh-oh, jouw browser is te oud om CodeCombat te kunnen spelen, Sorry!" # Warning that shows up on really old Firefox/Chrome/Safari old_browser: "uh-oh, jouw browser is te oud om CodeCombat te kunnen spelen, Sorry!" # Warning that shows up on really old Firefox/Chrome/Safari
old_browser_suffix: "Je kan alsnog proberen, maar het zal waarschijnlijk niet werken!" old_browser_suffix: "Je kan het alsnog proberen, maar het zal waarschijnlijk niet werken!"
ipad_browser: "Slecht nieuws: CodeCombat draait niet in je browser op de iPad. Goed nieuws: onze iPad-app wordt op het moment beoordeeld door Apple." ipad_browser: "Slecht nieuws: CodeCombat draait niet in je browser op de iPad. Goed nieuws: onze iPad-app wordt op het moment beoordeeld door Apple."
campaign: "Verhaallijn" campaign: "Verhaallijn"
for_beginners: "Voor Beginners" for_beginners: "Voor Beginners"
@ -16,7 +16,7 @@ module.exports = nativeDescription: "Nederlands (Nederland)", englishDescription
new_home: new_home:
slogan: "Het meest uitdagende spel om mee te leren programmeren." slogan: "Het meest uitdagende spel om mee te leren programmeren."
classroom_edition: "Klas versie:" classroom_edition: "Klasversie:"
learn_to_code: "Leer programmeren:" learn_to_code: "Leer programmeren:"
teacher: "Leraar" teacher: "Leraar"
student: "Leerling" student: "Leerling"
@ -48,10 +48,10 @@ module.exports = nativeDescription: "Nederlands (Nederland)", englishDescription
kind_of_struggle: "het soort worsteling dat uitmondt in een leerproces dat uitdagend is en " kind_of_struggle: "het soort worsteling dat uitmondt in een leerproces dat uitdagend is en "
motivating: "motiveert" motivating: "motiveert"
not_tedious: "niet vervelend." not_tedious: "niet vervelend."
gaming_is_good: "Studies geven aan dat speels leren goed is voor de hersenen van kinderen. (En dat klopt!)" gaming_is_good: "Studies wijzen uit dat speels leren goed is voor de hersenen van kinderen. (En dat klopt!)"
game_based: "Wanneer spel-gebaseerde leersystemen worden" game_based: "Wanneer spel-gebaseerde leersystemen worden"
compared: "vergeleken" compared: "vergeleken"
conventional: "met conventionele assessment methodes, is het verschil duidelijk: games zijn beter in het stimuleren van leerlingen bij het onthouden van kennis, concentratie en het" conventional: "met conventionele assessment methoden, is het verschil duidelijk: games zijn beter in het stimuleren van leerlingen bij het onthouden van kennis, concentratie en het"
perform_at_higher_level: "niveau van hun prestaties." perform_at_higher_level: "niveau van hun prestaties."
feedback: "Games verschaffen directe feedback, wat leerlingen in staat stelt hun oplossingen te verbeteren en zij een holistisch begrip van de concepten krijgen, in plaats van beperkt zijn tot antwoorden als “correct” of “incorrect”." feedback: "Games verschaffen directe feedback, wat leerlingen in staat stelt hun oplossingen te verbeteren en zij een holistisch begrip van de concepten krijgen, in plaats van beperkt zijn tot antwoorden als “correct” of “incorrect”."
real_game: "Een echt spel, wat je speelt met echte programmeertaal." real_game: "Een echt spel, wat je speelt met echte programmeertaal."
@ -1288,11 +1288,11 @@ module.exports = nativeDescription: "Nederlands (Nederland)", englishDescription
avg_student_exp_varied: "Verschilt enorm per leerling" avg_student_exp_varied: "Verschilt enorm per leerling"
student_age_range_label: "Leeftijdscategorie leerlingen" student_age_range_label: "Leeftijdscategorie leerlingen"
student_age_range_younger: "Jonger dan 6" student_age_range_younger: "Jonger dan 6"
student_age_range_older: "Ouden dan 18" student_age_range_older: "Ouder dan 18"
student_age_range_to: "tot" student_age_range_to: "tot"
create_class: "Maak klas aan" create_class: "Maak klas aan"
class_name: "Klasnaam" class_name: "Klasnaam"
teacher_account_restricted: "Jouw account is een Docenten Account, daarom heeft dit account geen toegang tot inhoud bedoelt voor leerlingen." teacher_account_restricted: "Jouw account is een Docenten Account, daarom heeft dit account geen toegang tot inhoud bedoeld voor leerlingen."
teacher: teacher:
teacher_dashboard: "Docent Dashboard" # Navbar teacher_dashboard: "Docent Dashboard" # Navbar
@ -1312,7 +1312,7 @@ module.exports = nativeDescription: "Nederlands (Nederland)", englishDescription
teacher_account_explanation: "Een CodeCombat Docenten account geeft je de mogelijkheid om klassen aan te maken, voortgang van leerlingen te bekijken terwijl ze de cursussen volgen, inschrijvingen beheren en hulpmiddelen te gebruiken voor het opzetten van een leerplan" teacher_account_explanation: "Een CodeCombat Docenten account geeft je de mogelijkheid om klassen aan te maken, voortgang van leerlingen te bekijken terwijl ze de cursussen volgen, inschrijvingen beheren en hulpmiddelen te gebruiken voor het opzetten van een leerplan"
current_classes: "Huidige Klassen" current_classes: "Huidige Klassen"
archived_classes: "Gearchiveerde Klassen" archived_classes: "Gearchiveerde Klassen"
archived_classes_blurb: "Klassen kunnen worden gearchiveerd voor toekomstige refferentie. Dearchiveer een klas om deze weer in de lijst Huidige Klassen te zien" archived_classes_blurb: "Klassen kunnen worden gearchiveerd voor toekomstige referentie. Dearchiveer een klas om deze weer in de lijst Huidige Klassen te zien"
view_class: "bekijk klas" view_class: "bekijk klas"
archive_class: "archiveer klas" archive_class: "archiveer klas"
unarchive_class: "dearchiveer klas" unarchive_class: "dearchiveer klas"
@ -1372,9 +1372,9 @@ module.exports = nativeDescription: "Nederlands (Nederland)", englishDescription
purchased: "Aanschaffing Voltooid!" purchased: "Aanschaffing Voltooid!"
purchase_now: "Nu Aanschaffen" purchase_now: "Nu Aanschaffen"
how_to_enroll: "Hoe Schrijf ik Leerlingen in" how_to_enroll: "Hoe Schrijf ik Leerlingen in"
how_to_enroll_blurb_1: "Als een leerling nog niet is ingeschreven zal er een \"Enroll\" knop naast hun cursus voortgang zijn in je klas." how_to_enroll_blurb_1: "Als een leerling nog niet is ingeschreven, staat er een knop genaamd \"Enroll\" bij hun naam."
how_to_enroll_blurb_2: "Om meerdere leerlingen in bulk in te schrijven, selecteer ze door middel van de selectievakjes aan de linkerkant van de klas pagina en klik op de \"Enroll Selected Students\" knop." how_to_enroll_blurb_2: "Je kunt meerdere leerlingen tegelijkertijd inschrijven. Vink simpelweg de vakjes aan voor de leerlingen die je wilt inschrijven en klik op de knop \"Enroll Selected Students\"."
how_to_enroll_blurb_3: "Zodra een leerling is ingeschreven zal deze toegang hebben tot alle inhoud van de cursus." how_to_enroll_blurb_3: "Zodra een leerling is ingeschreven heeft hij/zij toegang tot alle inhoud."
bulk_pricing_blurb: "Aanschaffen voor meer dan 25 leerlingen? Neem contact met ons op." bulk_pricing_blurb: "Aanschaffen voor meer dan 25 leerlingen? Neem contact met ons op."
total_unenrolled: "Totaal aantal niet ingeschreven" total_unenrolled: "Totaal aantal niet ingeschreven"
export_student_progress: "Exporteer Voortgang Leerlingen (CSV bestand)" export_student_progress: "Exporteer Voortgang Leerlingen (CSV bestand)"

View file

@ -38,7 +38,7 @@ module.exports = nativeDescription: "русский", englishDescription: "Russi
accessible_to: "Доступно для" accessible_to: "Доступно для"
everyone: "каждого" everyone: "каждого"
democratizing: "Демократизация процесса обучения программированию лежит в основе нашей философии. Каждый должен иметь возможность обучаться программированию." democratizing: "Демократизация процесса обучения программированию лежит в основе нашей философии. Каждый должен иметь возможность обучаться программированию."
# forgot_learning: "I think they actually forgot that they were actually learning something." forgot_learning: "Я думаю, что они забудут, что они действительно что-то изучают."
# wanted_to_do: " Coding is something I've always wanted to do, and I never thought I would be able to learn it in school." # wanted_to_do: " Coding is something I've always wanted to do, and I never thought I would be able to learn it in school."
# why_games: "Why is learning through games important?" # why_games: "Why is learning through games important?"
# games_reward: "Games reward the productive struggle." # games_reward: "Games reward the productive struggle."

View file

@ -0,0 +1,29 @@
CocoModel = require './CocoModel'
module.exports = class GameUIState extends CocoModel
@className: 'GameUIState'
@schema: {
type: 'object'
properties: {
canDragCamera: {
type: 'boolean'
description: 'Serves as a lock to enable or disable camera movement.'
}
selected: {
# TODO: Turn this into a collection which can be listened to? With Thang models.
type: 'object'
description: 'Array of selected thangs'
properties: {
sprite: { description: 'Lank instance' }
thang: { description: 'Thang object generated by the world' }
}
}
}
}
defaults: -> {
selected: []
canDragCamera: true
}

View file

@ -52,6 +52,7 @@ module.exports =
overallStatus: {type: ['string', 'null'], enum: ['success', 'failure', 'incomplete', null]} overallStatus: {type: ['string', 'null'], enum: ['success', 'failure', 'incomplete', null]}
totalFrames: {type: ['integer', 'undefined']} totalFrames: {type: ['integer', 'undefined']}
lastFrameHash: {type: ['number', 'undefined']} lastFrameHash: {type: ['number', 'undefined']}
simulationFrameRate: {type: ['number', 'undefined']}
'god:world-load-progress-changed': c.object {required: ['progress', 'god']}, 'god:world-load-progress-changed': c.object {required: ['progress', 'god']},
god: {type: 'object'} god: {type: 'object'}

View file

@ -387,7 +387,7 @@ block content
b CodeCombat Inc. b CodeCombat Inc.
p 301 Howard Street p 301 Howard Street
p Suite No. 830 p Suite No. 830
p San Francisco, California 94015 p San Francisco, California 94105
a(href="mailto:team@codecombat.com") team@codecombat.com a(href="mailto:team@codecombat.com") team@codecombat.com
.col-sm-4 .col-sm-4
p p

View file

@ -6,16 +6,30 @@ block content
if !me.isAdmin() if !me.isAdmin()
div You must be logged in as an admin to view this page. div You must be logged in as an admin to view this page.
else if !view.countryGraphs || !view.countryGraphs['USA']
h3 Loading...
else else
p CodeCombat is now in #{view.totalSchools} schools with #{view.totalStudents} students [and #{view.totalTeachers} teachers] [in #{view.totalStates} states] p
p Students not attached to NCES data: #{view.untriagedStudents} div CodeCombat is now in #{view.countryGraphs['USA'].totalSchools} schools with #{view.countryGraphs['USA'].totalStudents} students [and #{view.countryGraphs['USA'].totalTeachers} teachers] [in #{view.countryGraphs['USA'].totalStates} states] in the USA
p
div Untriaged students: #{view.untriagedStudents}
div Untriaged teachers: #{view.untriagedTeachers}
.small Teacher: owns a classroom or has a teacher role .small Teacher: owns a classroom or has a teacher role
.small Student: member of a classroom or has schoolName set .small Student: member of a classroom or has schoolName set, not in HoC course instance
.small States, Districts, Schools are from NCES .small +3 USA states are GU, PR, DC
h2 State Counts p
if view.stateCounts ul
li
a(href="#usaStates") USA States
li
a(href="#usaDistrictsByState") USA Districts by State
li
a(href="#countries") Countries
a(name="usaStates")
h2 USA States
if view.countryGraphs['USA'].stateCounts
table.table.table-striped.table-condensed table.table.table-striped.table-condensed
tr tr
th State th State
@ -23,7 +37,7 @@ block content
th Schools th Schools
th Teachers th Teachers
th Students th Students
each stateCount in view.stateCounts each stateCount in view.countryGraphs['USA'].stateCounts
tr tr
td= stateCount.state td= stateCount.state
td= stateCount.districts td= stateCount.districts
@ -31,8 +45,9 @@ block content
td= stateCount.teachers td= stateCount.teachers
td= stateCount.students td= stateCount.students
h2 District Counts by State a(name="usaDistrictsByState")
if view.districtCounts h2 USA Districts by State
if view.countryGraphs['USA'].districtCounts
table.table.table-striped.table-condensed table.table.table-striped.table-condensed
tr tr
th State th State
@ -40,10 +55,26 @@ block content
th Schools th Schools
th Teachers th Teachers
th Students th Students
each districtCount in view.districtCounts each districtCount in view.countryGraphs['USA'].districtCounts
tr tr
td= districtCount.state td= districtCount.state
td= districtCount.district td= districtCount.district
td= districtCount.schools td= districtCount.schools
td= districtCount.teachers td= districtCount.teachers
td= districtCount.students td= districtCount.students
a(name="countries")
h2 Countries
if view.countryCounts
table.table.table-striped.table-condensed
tr
th Country
th Schools
th Teachers
th Students
each countryCount in view.countryCounts
tr
td= countryCount.country
td= countryCount.schools
td= countryCount.teachers
td= countryCount.students

View file

@ -105,6 +105,14 @@ block content
h4.test-failed User Code Problems h4.test-failed User Code Problems
pre.test-failed #{JSON.stringify(test.userCodeProblems, null, 2)} pre.test-failed #{JSON.stringify(test.userCodeProblems, null, 2)}
if test.simulationFrameRate
if test.simulationFrameRate > 90
div.test-success ✓ #{test.simulationFrameRate.toFixed(1)} FPS
else if test.simulationFrameRate > 30
div.test-running ~ #{test.simulationFrameRate.toFixed(1)} FPS
else
div.test-failed ✘ #{test.simulationFrameRate.toFixed(1)} FPS
else else
h1 Loading Level... h1 Loading Level...

View file

@ -1,10 +1,11 @@
RootView = require 'views/core/RootView' RootView = require 'views/core/RootView'
CocoCollection = require 'collections/CocoCollection' CocoCollection = require 'collections/CocoCollection'
Classroom = require 'models/Classroom' Classroom = require 'models/Classroom'
CourseInstance = require 'models/CourseInstance'
TrialRequest = require 'models/TrialRequest' TrialRequest = require 'models/TrialRequest'
User = require 'models/User' User = require 'models/User'
utils = require 'core/utils'
# TODO: trim orphaned students: course instances != Single Player, hourOfCode != true
# TODO: match anonymous trial requests with real users via email # TODO: match anonymous trial requests with real users via email
module.exports = class SchoolCountsView extends RootView module.exports = class SchoolCountsView extends RootView
@ -15,6 +16,8 @@ module.exports = class SchoolCountsView extends RootView
return super() unless me.isAdmin() return super() unless me.isAdmin()
@classrooms = new CocoCollection([], { url: "/db/classroom/-/users", model: Classroom }) @classrooms = new CocoCollection([], { url: "/db/classroom/-/users", model: Classroom })
@supermodel.loadCollection(@classrooms, 'classrooms', {cache: false}) @supermodel.loadCollection(@classrooms, 'classrooms', {cache: false})
@courseInstances = new CocoCollection([], { url: "/db/course_instance/-/non-hoc", model: CourseInstance})
@supermodel.loadCollection(@courseInstances, 'course-instances', {cache: false})
@students = new CocoCollection([], { url: "/db/user/-/students", model: User }) @students = new CocoCollection([], { url: "/db/user/-/students", model: User })
@supermodel.loadCollection(@students, 'students', {cache: false}) @supermodel.loadCollection(@students, 'students', {cache: false})
@teachers = new CocoCollection([], { url: "/db/user/-/teachers", model: User }) @teachers = new CocoCollection([], { url: "/db/user/-/teachers", model: User })
@ -30,39 +33,42 @@ module.exports = class SchoolCountsView extends RootView
teacherMap = {} # Used to make sure teachers and students only counted once teacherMap = {} # Used to make sure teachers and students only counted once
studentMap = {} # Used to make sure teachers and students only counted once studentMap = {} # Used to make sure teachers and students only counted once
studentNonHocMap = {} # Used to exclude HoC users
teacherStudentMap = {} # Used to link students to their teacher locations teacherStudentMap = {} # Used to link students to their teacher locations
orphanedSchoolStudentMap = {} # Used to link student schoolName to teacher Nces data
countryStateDistrictSchoolCountsMap = {} # Data graph countryStateDistrictSchoolCountsMap = {} # Data graph
console.log(new Date().toISOString(), 'Processing classrooms...') console.log(new Date().toISOString(), "Processing #{@courseInstances.models.length} course instances...")
for courseInstance in @courseInstances.models
studentNonHocMap[courseInstance.get('ownerID')] = true
studentNonHocMap[studentID] = true for studentID in courseInstance.get('members') ? []
console.log(new Date().toISOString(), "Processing #{@classrooms.models.length} classrooms...")
for classroom in @classrooms.models for classroom in @classrooms.models
teacherID = classroom.get('ownerID') teacherID = classroom.get('ownerID')
teacherMap[teacherID] ?= {} teacherMap[teacherID] ?= {}
teacherMap[teacherID] = true teacherMap[teacherID] = true
teacherStudentMap[teacherID] ?= {} teacherStudentMap[teacherID] ?= {}
for studentID in classroom.get('members') for studentID in classroom.get('members')
continue unless studentNonHocMap[studentID]
studentMap[studentID] = true studentMap[studentID] = true
teacherStudentMap[teacherID][studentID] = true teacherStudentMap[teacherID][studentID] = true
console.log(new Date().toISOString(), 'Processing teachers...') console.log(new Date().toISOString(), "Processing #{@teachers.models.length} teachers...")
for teacher in @teachers.models for teacher in @teachers.models
teacherMap[teacher.id] ?= {} teacherMap[teacher.id] ?= {}
delete studentMap[teacher.id] delete studentMap[teacher.id]
console.log(new Date().toISOString(), 'Processing students...') console.log(new Date().toISOString(), "Processing #{@students.models.length} students...")
for student in @students.models when not teacherMap[student.id] for student in @students.models when not teacherMap[student.id]
continue unless studentNonHocMap[student.id]
schoolName = student.get('schoolName') schoolName = student.get('schoolName')
studentMap[student.id] = true studentMap[student.id] = true
orphanedSchoolStudentMap[schoolName] ?= {}
orphanedSchoolStudentMap[schoolName][student.id] = true
console.log(new Date().toISOString(), 'Processing trial requests...') console.log(new Date().toISOString(), "Processing trial #{@trialRequests.models.length} requests...")
# TODO: this step is crazy slow
orphanSchoolsMatched = 0
orphanStudentsMatched = 0
for trialRequest in @trialRequests.models for trialRequest in @trialRequests.models
teacherID = trialRequest.get('applicant') teacherID = trialRequest.get('applicant')
unless teacherMap[teacherID] unless teacherMap[teacherID]
# E.g. parents
# console.log("Skipping non-teacher #{teacherID} trial request #{trialRequest.id}") # console.log("Skipping non-teacher #{teacherID} trial request #{trialRequest.id}")
continue continue
props = trialRequest.get('properties') props = trialRequest.get('properties')
@ -78,27 +84,44 @@ module.exports = class SchoolCountsView extends RootView
countryStateDistrictSchoolCountsMap[country][state][district][school].teachers[teacherID] = true countryStateDistrictSchoolCountsMap[country][state][district][school].teachers[teacherID] = true
for studentID, val of teacherStudentMap[teacherID] for studentID, val of teacherStudentMap[teacherID]
countryStateDistrictSchoolCountsMap[country][state][district][school].students[studentID] = true countryStateDistrictSchoolCountsMap[country][state][district][school].students[studentID] = true
for orphanSchool, students of orphanedSchoolStudentMap else if not _.isEmpty(props.country)
if school is orphanSchool or school.replace(/unified|elementary|high|district|#\d+|isd|unified district|school district/ig, '').trim() is orphanSchool.trim() country = props.country?.trim()
orphanSchoolsMatched++ country = country[0].toUpperCase() + country.substring(1).toLowerCase()
for studentID, val of students country = 'Taiwan' if /台灣/ig.test(country)
orphanStudentsMatched++ country = 'UK' if /^uk$|united kingdom|england/ig.test(country)
countryStateDistrictSchoolCountsMap[country][state][district][school].students[studentID] = true country = 'USA' if /^u\.s\.?(\.a)?\.?$|^us$|america|united states|usa/ig.test(country)
delete orphanedSchoolStudentMap[school] state = props.state ? 'unknown'
console.log(new Date().toISOString(), "#{orphanSchoolsMatched} orphanSchoolsMatched #{orphanStudentsMatched} orphanStudentsMatched") if country is 'USA'
stateName = utils.usStateCodes.sanitizeStateName(state)
state = utils.usStateCodes.getStateCodeByStateName(stateName) if stateName
state = utils.usStateCodes.sanitizeStateCode(state) ? state
district = 'unknown'
school = props.organiziation ? 'unknown'
countryStateDistrictSchoolCountsMap[country] ?= {}
countryStateDistrictSchoolCountsMap[country][state] ?= {}
countryStateDistrictSchoolCountsMap[country][state][district] ?= {}
countryStateDistrictSchoolCountsMap[country][state][district][school] ?= {students: {}, teachers: {}}
countryStateDistrictSchoolCountsMap[country][state][district][school].teachers[teacherID] = true
for studentID, val of teacherStudentMap[teacherID]
countryStateDistrictSchoolCountsMap[country][state][district][school].students[studentID] = true
console.log(new Date().toISOString(), 'Building graph...') console.log(new Date().toISOString(), 'Building country graphs...')
@totalSchools = 0 @countryGraphs = {}
@totalStudents = 0 @countryCounts = []
@totalTeachers = 0 totalStudents = 0
@totalStates = 0 totalTeachers = 0
@stateCounts = []
stateCountsMap = {}
@districtCounts = []
for country, stateDistrictSchoolCountsMap of countryStateDistrictSchoolCountsMap for country, stateDistrictSchoolCountsMap of countryStateDistrictSchoolCountsMap
continue unless /usa/ig.test(country) @countryGraphs[country] =
districtCounts: []
stateCounts: []
stateCountsMap: {}
totalSchools: 0
totalStates: 0
totalStudents: 0
totalTeachers: 0
for state, districtSchoolCountsMap of stateDistrictSchoolCountsMap for state, districtSchoolCountsMap of stateDistrictSchoolCountsMap
@totalStates++ if utils.usStateCodes.sanitizeStateCode(state)? or ['GU', 'PR'].indexOf(state) >= 0
@countryGraphs[country].totalStates++
stateData = {state: state, districts: 0, schools: 0, students: 0, teachers: 0} stateData = {state: state, districts: 0, schools: 0, students: 0, teachers: 0}
for district, schoolCountsMap of districtSchoolCountsMap for district, schoolCountsMap of districtSchoolCountsMap
stateData.districts++ stateData.districts++
@ -106,39 +129,40 @@ module.exports = class SchoolCountsView extends RootView
for school, counts of schoolCountsMap for school, counts of schoolCountsMap
studentCount = Object.keys(counts.students).length studentCount = Object.keys(counts.students).length
teacherCount = Object.keys(counts.teachers).length teacherCount = Object.keys(counts.teachers).length
@totalSchools++ @countryGraphs[country].totalSchools++
@totalStudents += studentCount @countryGraphs[country].totalStudents += studentCount
@totalTeachers += teacherCount @countryGraphs[country].totalTeachers += teacherCount
stateData.schools++ stateData.schools++
stateData.students += studentCount stateData.students += studentCount
stateData.teachers += teacherCount stateData.teachers += teacherCount
districtData.schools++ districtData.schools++
districtData.students += studentCount districtData.students += studentCount
districtData.teachers += teacherCount districtData.teachers += teacherCount
@districtCounts.push(districtData) @countryGraphs[country].districtCounts.push(districtData)
@stateCounts.push(stateData) @countryGraphs[country].stateCounts.push(stateData)
stateCountsMap[state] = stateData @countryGraphs[country].stateCountsMap[state] = stateData
@untriagedStudents = Object.keys(studentMap).length - @totalStudents @countryCounts.push
country: country
schools: @countryGraphs[country].totalSchools
students: @countryGraphs[country].totalStudents
teachers: @countryGraphs[country].totalTeachers
totalStudents += @countryGraphs[country].totalSchools
totalTeachers += @countryGraphs[country].totalTeachers
@untriagedStudents = Object.keys(studentMap).length - totalStudents
@untriagedTeachers = Object.keys(teacherMap).length - totalTeachers
@stateCounts.sort (a, b) -> for country, graph of @countryGraphs
return -1 if a.students > b.students graph.stateCounts.sort (a, b) ->
return 1 if a.students < b.students b.students - a.students or b.teachers - a.teachers or b.schools - a.schools or b.districts - a.districts or b.state.localeCompare(a.state)
return -1 if a.teachers > b.teachers graph.districtCounts.sort (a, b) ->
return 1 if a.teachers < b.teachers if a.state isnt b.state
return -1 if a.districts > b.districts stateCountsA = graph.stateCountsMap[a.state]
return 1 if a.districts < b.districts stateCountsB = graph.stateCountsMap[b.state]
b.state.localeCompare(a.state) stateCountsB.students - stateCountsA.students or stateCountsB.teachers - stateCountsA.teachers or stateCountsB.schools - stateCountsA.schools or stateCountsB.districts - stateCountsA.districts or a.state.localeCompare(b.state)
@districtCounts.sort (a, b) -> else
if a.state isnt b.state b.students - a.students or b.teachers - a.teachers or b.schools - a.schools or b.district.localeCompare(a.district)
return -1 if stateCountsMap[a.state].students > stateCountsMap[b.state].students @countryCounts.sort (a, b) ->
return 1 if stateCountsMap[a.state].students < stateCountsMap[b.state].students b.students - a.students or b.teachers - a.teachers or b.schools - a.schools or b.country.localeCompare(a.country)
return -1 if stateCountsMap[a.state].teachers > stateCountsMap[b.state].teachers
return 1 if stateCountsMap[a.state].teachers < stateCountsMap[b.state].teachers console.log(new Date().toISOString(), 'Done...')
a.state.localeCompare(b.state)
else
return -1 if a.students > b.students
return 1 if a.students < b.students
return -1 if a.teachers > b.teachers
return 1 if a.teachers < b.teachers
a.district.localeCompare(b.district)
super() super()

View file

@ -11,6 +11,7 @@ Thang = require 'lib/world/thang'
LevelThangEditView = require './LevelThangEditView' LevelThangEditView = require './LevelThangEditView'
ComponentsCollection = require 'collections/ComponentsCollection' ComponentsCollection = require 'collections/ComponentsCollection'
require 'vendor/treema' require 'vendor/treema'
GameUIState = require 'models/GameUIState'
# Moving the screen while dragging thangs constants # Moving the screen while dragging thangs constants
MOVE_MARGIN = 0.15 MOVE_MARGIN = 0.15
@ -29,7 +30,6 @@ module.exports = class ThangsTabView extends CocoView
template: thangs_template template: thangs_template
subscriptions: subscriptions:
'surface:sprite-selected': 'onExtantThangSelected'
'surface:mouse-moved': 'onSurfaceMouseMoved' 'surface:mouse-moved': 'onSurfaceMouseMoved'
'surface:mouse-over': 'onSurfaceMouseOver' 'surface:mouse-over': 'onSurfaceMouseOver'
'surface:mouse-out': 'onSurfaceMouseOut' 'surface:mouse-out': 'onSurfaceMouseOut'
@ -39,7 +39,6 @@ module.exports = class ThangsTabView extends CocoView
'editor:view-switched': 'onViewSwitched' 'editor:view-switched': 'onViewSwitched'
'sprite:dragged': 'onSpriteDragged' 'sprite:dragged': 'onSpriteDragged'
'sprite:mouse-up': 'onSpriteMouseUp' 'sprite:mouse-up': 'onSpriteMouseUp'
'sprite:mouse-down': 'onSpriteMouseDown'
'sprite:double-clicked': 'onSpriteDoubleClicked' 'sprite:double-clicked': 'onSpriteDoubleClicked'
'surface:stage-mouse-down': 'onStageMouseDown' 'surface:stage-mouse-down': 'onStageMouseDown'
'surface:stage-mouse-up': 'onStageMouseUp' 'surface:stage-mouse-up': 'onStageMouseUp'
@ -78,12 +77,17 @@ module.exports = class ThangsTabView extends CocoView
constructor: (options) -> constructor: (options) ->
super options super options
@world = options.world @world = options.world
@gameUIState = new GameUIState()
@listenTo(@gameUIState, 'sprite:mouse-down', @onSpriteMouseDown)
@listenTo(@gameUIState, 'surface:stage-mouse-move', @onStageMouseMove)
@listenTo(@gameUIState, 'change:selected', @onChangeSelected)
# should load depended-on Components, too # should load depended-on Components, too
@thangTypes = @supermodel.loadCollection(new ThangTypeSearchCollection(), 'thangs').model @thangTypes = @supermodel.loadCollection(new ThangTypeSearchCollection(), 'thangs').model
# just loading all Components for now: https://github.com/codecombat/codecombat/issues/405 # just loading all Components for now: https://github.com/codecombat/codecombat/issues/405
@componentCollection = @supermodel.loadCollection(new ComponentsCollection(), 'components').load() @componentCollection = @supermodel.loadCollection(new ComponentsCollection(), 'components').load()
@level = options.level @level = options.level
@onThangsChanged = _.debounce(@onThangsChanged)
$(document).bind 'contextmenu', @preventDefaultContextMenu $(document).bind 'contextmenu', @preventDefaultContextMenu
@ -203,7 +207,7 @@ module.exports = class ThangsTabView extends CocoView
initSurface: -> initSurface: ->
webGLCanvas = $('canvas#webgl-surface', @$el) webGLCanvas = $('canvas#webgl-surface', @$el)
normalCanvas = $('canvas#normal-surface', @$el) normalCanvas = $('canvas#normal-surface', @$el)
@surface = new Surface @world, normalCanvas, webGLCanvas, { @surface = new Surface(@world, normalCanvas, webGLCanvas, {
paths: false paths: false
coords: true coords: true
grid: true grid: true
@ -212,7 +216,9 @@ module.exports = class ThangsTabView extends CocoView
showInvisible: true showInvisible: true
frameRate: 15 frameRate: 15
levelType: @level.get 'type', true levelType: @level.get 'type', true
} @gameUIState
handleEvents: false
})
@surface.playing = false @surface.playing = false
@surface.setWorld @world @surface.setWorld @world
@surface.lankBoss.suppressSelectionSounds = true @surface.lankBoss.suppressSelectionSounds = true
@ -240,38 +246,79 @@ module.exports = class ThangsTabView extends CocoView
@selectAddThang null, true @selectAddThang null, true
@surface?.lankBoss?.selectLank null, null @surface?.lankBoss?.selectLank null, null
onSpriteMouseDown: (e) ->
@dragged = false
# Sprite clicks happen after stage clicks, but we need to know whether a sprite is being clicked.
# clearTimeout @backgroundAddClickTimeout
# if e.originalEvent.nativeEvent.button == 2
# @onSpriteContextMenu e
onStageMouseDown: (e) -> onStageMouseDown: (e) ->
return unless @addThangLank?.thangType.get('kind') is 'Wall' # initial values for a mouse click lifecycle
@surface.camera.dragDisabled = true @dragged = 0
@paintingWalls = true @willUnselectSprite = false
@gameUIState.set('canDragCamera', true)
if @addThangLank?.thangType.get('kind') is 'Wall'
@paintingWalls = true
@gameUIState.set('canDragCamera', false)
else if @addThangLank
# We clicked on the background when we had an add Thang selected, so add it
@addThang @addThangType, @addThangLank.thang.pos
else if e.onBackground
@gameUIState.set('selected', [])
onStageMouseMove: (e) ->
@dragged += 1
onStageMouseUp: (e) -> onStageMouseUp: (e) ->
if @paintingWalls @paintingWalls = false
# We need to stop painting walls, but we may also stop in onExtantThangSelected.
_.defer =>
@paintingWalls = @paintedWalls = @surface.camera.dragDisabled = false
else if @addThangLank
@surface.camera.lock()
# If we click on the background, we need to add @addThangLank, but not if onSpriteMouseUp will fire.
@backgroundAddClickTimeout = _.defer => @onExtantThangSelected {}
$('#contextmenu').hide() $('#contextmenu').hide()
onSpriteMouseDown: (e) ->
nativeEvent = e.originalEvent.nativeEvent
# update selection
selected = []
if nativeEvent.metaKey or nativeEvent.ctrlKey
selected = _.clone(@gameUIState.get('selected'))
if e.thang?.isSelectable
alreadySelected = _.find(selected, (s) -> s.thang is e.thang)
if alreadySelected
# move to end (make it the last selected) and maybe unselect it
@willUnselectSprite = true
selected = _.without(selected, alreadySelected)
selected.push({ thang: e.thang, sprite: e.sprite, spellName: e.spellName })
if _.any(selected) and key.alt
# Clone selected thang instead of selecting it
lastSelected = _.last(selected)
@selectAddThangType lastSelected.thang.spriteName, lastSelected.thang
selected = []
@gameUIState.set('selected', selected)
if _.any(selected)
@gameUIState.set('canDragCamera', false)
onSpriteDragged: (e) -> onSpriteDragged: (e) ->
return unless @selectedExtantThang and e.thang?.id is @selectedExtantThang?.id selected = @gameUIState.get('selected')
@dragged = true return unless _.any(selected) and @dragged > 10
@surface.camera.dragDisabled = true @willUnselectSprite = false
{stageX, stageY} = e.originalEvent {stageX, stageY} = e.originalEvent
# move the one under the mouse
lastSelected = _.last(selected)
cap = @surface.camera.screenToCanvas x: stageX, y: stageY cap = @surface.camera.screenToCanvas x: stageX, y: stageY
wop = @surface.camera.canvasToWorld cap wop = @surface.camera.canvasToWorld cap
wop.z = @selectedExtantThang.depth / 2 wop.z = lastSelected.thang.depth / 2
@adjustThangPos @selectedExtantLank, @selectedExtantThang, wop posBefore = _.clone(lastSelected.thang.pos)
@adjustThangPos lastSelected.sprite, lastSelected.thang, wop
posAfter = lastSelected.thang.pos
# move any others selected, proportionally to how the 'main' sprite moved
xDiff = posAfter.x - posBefore.x
yDiff = posAfter.y - posBefore.y
if xDiff or yDiff
for singleSelected in selected.slice(0, selected.length - 1)
newPos = {
x: singleSelected.thang.pos.x + xDiff
y: singleSelected.thang.pos.y + yDiff
}
@adjustThangPos singleSelected.sprite, singleSelected.thang, newPos
# move the camera if we're on the edge of the screen
[w, h] = [@surface.camera.canvasWidth, @surface.camera.canvasHeight] [w, h] = [@surface.camera.canvasWidth, @surface.camera.canvasHeight]
sidebarWidths = ((if @$el.find(id).hasClass('hide') then 0 else (@$el.find(id).outerWidth() / @surface.camera.canvasScaleFactorX)) for id in ['#all-thangs', '#add-thangs-view']) sidebarWidths = ((if @$el.find(id).hasClass('hide') then 0 else (@$el.find(id).outerWidth() / @surface.camera.canvasScaleFactorX)) for id in ['#all-thangs', '#add-thangs-view'])
w -= sidebarWidth for sidebarWidth in sidebarWidths w -= sidebarWidth for sidebarWidth in sidebarWidths
@ -279,24 +326,30 @@ module.exports = class ThangsTabView extends CocoView
@calculateMovement(cap.x / w, cap.y / h, w / h) @calculateMovement(cap.x / w, cap.y / h, w / h)
onSpriteMouseUp: (e) -> onSpriteMouseUp: (e) ->
clearTimeout @backgroundAddClickTimeout selected = @gameUIState.get('selected')
@surface.camera.unlock() if e.originalEvent.nativeEvent.button == 2 and _.any(selected)
if e.originalEvent.nativeEvent.button == 2 and @selectedExtantThang
@onSpriteContextMenu e @onSpriteContextMenu e
clearInterval(@movementInterval) if @movementInterval? clearInterval(@movementInterval) if @movementInterval?
@movementInterval = null @movementInterval = null
@surface.camera.dragDisabled = false
return unless @selectedExtantThang and e.thang?.id is @selectedExtantThang?.id
pos = @selectedExtantThang.pos
thang = _.find(@level.get('thangs') ? [], {id: @selectedExtantThang.id}) return unless _.any(selected)
path = "#{@pathForThang(thang)}/components/original=#{LevelComponent.PhysicalID}"
physical = @thangsTreema.get path for singleSelected in selected
return if not physical or (physical.config.pos.x is pos.x and physical.config.pos.y is pos.y) pos = singleSelected.thang.pos
@thangsTreema.set path + '/config/pos', x: pos.x, y: pos.y, z: pos.z
thang = _.find(@level.get('thangs') ? [], {id: singleSelected.thang.id})
path = "#{@pathForThang(thang)}/components/original=#{LevelComponent.PhysicalID}"
physical = @thangsTreema.get path
continue if not physical or (physical.config.pos.x is pos.x and physical.config.pos.y is pos.y)
@thangsTreema.set path + '/config/pos', x: pos.x, y: pos.y, z: pos.z
if @willUnselectSprite
clickedSprite = _.find(selected, {sprite: e.sprite})
@gameUIState.set('selected', _.without(selected, clickedSprite))
onSpriteDoubleClicked: (e) -> onSpriteDoubleClicked: (e) ->
return unless e.thang and not @dragged return if @dragged > 10
return unless e.thang
@editThang thangID: e.thang.id @editThang thangID: e.thang.id
onRandomTerrainGenerated: (e) -> onRandomTerrainGenerated: (e) ->
@ -320,35 +373,21 @@ module.exports = class ThangsTabView extends CocoView
@onThangsChanged() @onThangsChanged()
@selectAddThangType null @selectAddThangType null
onChangeSelected: (gameUIState, selected) ->
previousSprite = gameUIState.previousAttributes()?.selected?.sprite
sprite = selected?.sprite
thang = selected?.thang
# TODO: figure out a good way to have all Surface clicks and Treema clicks just proxy in one direction, so we can maintain only one way of handling selection and deletion previousSprite?.setNameLabel?(null) unless previousSprite is sprite
onExtantThangSelected: (e) ->
@selectedExtantLank?.setNameLabel? null unless @selectedExtantLank is e.sprite if thang and not (@addThangLank and @addThangType.get('name') in overlappableThangTypeNames)
@selectedExtantThang = e.thang
@selectedExtantLank = e.sprite
paintedAWall = @paintedWalls
@paintingWalls = @paintedWalls = @surface.camera.dragDisabled = false
if paintedAWall
# Skip adding a wall now, because we already dragged to add one
null
else if e.thang and (key.alt or key.meta)
# We alt-clicked, so create a clone addThang
@selectAddThangType e.thang.spriteName, @selectedExtantThang
else if @justAdded()
# Skip double insert due to extra selection event
null
else if e.thang and not (@addThangLank and @addThangType.get('name') in overlappableThangTypeNames)
# We clicked on a Thang (or its Treema), so select the Thang # We clicked on a Thang (or its Treema), so select the Thang
@selectAddThang null, true @selectAddThang(null, true)
@selectedExtantThangClickTime = new Date() @selectedExtantThangClickTime = new Date()
# Show the label above selected thang, notice that we may get here from thang-edit-view, so it will be selected but no label # Show the label above selected thang, notice that we may get here from thang-edit-view, so it will be selected but no label
@selectedExtantLank.setNameLabel @selectedExtantLank.thangType.get('name') + ': ' + @selectedExtantThang.id sprite.setNameLabel(sprite.thangType.get('name') + ': ' + thang.id)
@selectedExtantLank.updateLabels() sprite.updateLabels()
@selectedExtantLank.updateMarks() sprite.updateMarks()
else if @addThangLank
# We clicked on the background when we had an add Thang selected, so add it
@addThang @addThangType, @addThangLank.thang.pos
@lastAddTime = new Date()
justAdded: -> @lastAddTime and (new Date() - @lastAddTime) < 150 justAdded: -> @lastAddTime and (new Date() - @lastAddTime) < 150
@ -482,11 +521,14 @@ module.exports = class ThangsTabView extends CocoView
deleteSelectedExtantThang: (e) => deleteSelectedExtantThang: (e) =>
return if $(e.target).hasClass 'treema-node' return if $(e.target).hasClass 'treema-node'
return unless @selectedExtantThang selected = @gameUIState.get('selected')
thang = @getThangByID(@selectedExtantThang.id) return unless _.any(selected)
@thangsTreema.delete(@pathForThang(thang))
@deleteEmptyTreema(thang) for singleSelected in selected
Thang.resetThangIDs() # TODO: find some way to do this when we delete from treema, too thang = @getThangByID(singleSelected.thang.id)
@thangsTreema.delete(@pathForThang(thang))
@deleteEmptyTreema(thang)
Thang.resetThangIDs() # TODO: find some way to do this when we delete from treema, too
deleteEmptyTreema: (thang)-> deleteEmptyTreema: (thang)->
thangType = @supermodel.getModelByOriginal ThangType, thang.thangType thangType = @supermodel.getModelByOriginal ThangType, thang.thangType
@ -564,17 +606,26 @@ module.exports = class ThangsTabView extends CocoView
@selectAddThangType @addThangType, @cloneSourceThang if @addThangType # make another addThang sprite, since the World just refreshed @selectAddThangType @addThangType, @cloneSourceThang if @addThangType # make another addThang sprite, since the World just refreshed
# update selection, since the thangs have been remade # update selection, since the thangs have been remade
if @selectedExtantThang selected = @gameUIState.get('selected')
@selectedExtantLank = @surface.lankBoss.lanks[@selectedExtantThang.id] if _.any(selected)
@selectedExtantThang = @selectedExtantLank?.thang for singleSelected in selected
sprite = @surface.lankBoss.lanks[singleSelected.thang.id]
if sprite
sprite.updateMarks()
singleSelected.sprite = sprite
singleSelected.thang = sprite.thang
Backbone.Mediator.publish 'editor:thangs-edited', thangs: @world.thangs Backbone.Mediator.publish 'editor:thangs-edited', thangs: @world.thangs
onTreemaThangSelected: (e, selectedTreemas) => onTreemaThangSelected: (e, selectedTreemas) =>
selectedThangID = _.last(selectedTreemas)?.data.id selectedThangTreemas = _.filter(selectedTreemas, (t) -> t instanceof ThangNode)
if selectedThangID isnt @selectedExtantThang?.id thangIDs = (node.data.id for node in selectedThangTreemas)
@surface.lankBoss.selectThang selectedThangID, null, true lanks = (@surface.lankBoss.lanks[thangID] for thangID in thangIDs when thangID)
selected = ({ thang: lank.thang, sprite: lank } for lank in lanks when lank)
@gameUIState.set('selected', selected)
onTreemaThangDoubleClicked: (e, treema) => onTreemaThangDoubleClicked: (e, treema) =>
nativeEvent = e.originalEvent.nativeEvent
return if nativeEvent and (nativeEvent.ctrlKey or nativeEvent.metaKey)
id = treema?.data?.id id = treema?.data?.id
@editThang thangID: id if id @editThang thangID: id if id
@ -655,7 +706,8 @@ module.exports = class ThangsTabView extends CocoView
onDuplicateClicked: (e) -> onDuplicateClicked: (e) ->
$('#contextmenu').hide() $('#contextmenu').hide()
@selectAddThangType @selectedExtantThang.spriteName, @selectedExtantThang selected = _.last(@gameUIState.get('selected'))
@selectAddThangType(selected.thang.spriteName, selected.thang)
onClickRotationButton: (e) -> onClickRotationButton: (e) ->
$('#contextmenu').hide() $('#contextmenu').hide()
@ -667,7 +719,8 @@ module.exports = class ThangsTabView extends CocoView
@hush = true @hush = true
thangData = @getThangByID thang.id thangData = @getThangByID thang.id
thangData = $.extend true, {}, thangData thangData = $.extend true, {}, thangData
unless component = _.find thangData.components, {original: componentOriginal} component = _.find thangData.components, {original: componentOriginal}
unless component
component = original: componentOriginal, config: {}, majorVersion: 0 component = original: componentOriginal, config: {}, majorVersion: 0
thangData.components.push component thangData.components.push component
modificationFunction component modificationFunction component
@ -682,34 +735,44 @@ module.exports = class ThangsTabView extends CocoView
lank.setDebug true lank.setDebug true
rotateSelectedThangTo: (radians) -> rotateSelectedThangTo: (radians) ->
@modifySelectedThangComponentConfig @selectedExtantThang, LevelComponent.PhysicalID, (component) => for singleSelected in @gameUIState.get('selected')
component.config.rotation = radians selectedThang = singleSelected.thang
@selectedExtantThang.rotation = component.config.rotation @modifySelectedThangComponentConfig selectedThang, LevelComponent.PhysicalID, (component) =>
component.config.rotation = radians
selectedThang.rotation = component.config.rotation
rotateSelectedThangBy: (radians) -> rotateSelectedThangBy: (radians) ->
@modifySelectedThangComponentConfig @selectedExtantThang, LevelComponent.PhysicalID, (component) => for singleSelected in @gameUIState.get('selected')
component.config.rotation = ((component.config.rotation ? 0) + radians) % (2 * Math.PI) selectedThang = singleSelected.thang
@selectedExtantThang.rotation = component.config.rotation @modifySelectedThangComponentConfig selectedThang, LevelComponent.PhysicalID, (component) =>
component.config.rotation = ((component.config.rotation ? 0) + radians) % (2 * Math.PI)
selectedThang.rotation = component.config.rotation
moveSelectedThangBy: (xDir, yDir) -> moveSelectedThangBy: (xDir, yDir) ->
@modifySelectedThangComponentConfig @selectedExtantThang, LevelComponent.PhysicalID, (component) => for singleSelected in @gameUIState.get('selected')
component.config.pos.x += 0.5 * xDir selectedThang = singleSelected.thang
component.config.pos.y += 0.5 * yDir @modifySelectedThangComponentConfig selectedThang, LevelComponent.PhysicalID, (component) =>
@selectedExtantThang.pos.x = component.config.pos.x component.config.pos.x += 0.5 * xDir
@selectedExtantThang.pos.y = component.config.pos.y component.config.pos.y += 0.5 * yDir
selectedThang.pos.x = component.config.pos.x
selectedThang.pos.y = component.config.pos.y
resizeSelectedThangBy: (xDir, yDir) -> resizeSelectedThangBy: (xDir, yDir) ->
@modifySelectedThangComponentConfig @selectedExtantThang, LevelComponent.PhysicalID, (component) => for singleSelected in @gameUIState.get('selected')
component.config.width = (component.config.width ? 4) + 0.5 * xDir selectedThang = singleSelected.thang
component.config.height = (component.config.height ? 4) + 0.5 * yDir @modifySelectedThangComponentConfig selectedThang, LevelComponent.PhysicalID, (component) =>
@selectedExtantThang.width = component.config.width component.config.width = (component.config.width ? 4) + 0.5 * xDir
@selectedExtantThang.height = component.config.height component.config.height = (component.config.height ? 4) + 0.5 * yDir
selectedThang.width = component.config.width
selectedThang.height = component.config.height
toggleSelectedThangCollision: -> toggleSelectedThangCollision: ->
@modifySelectedThangComponentConfig @selectedExtantThang, LevelComponent.CollidesID, (component) => for singleSelected in @gameUIState.get('selected')
component.config ?= {} selectedThang = singleSelected.thang
component.config.collisionCategory = if component.config.collisionCategory is 'none' then 'ground' else 'none' @modifySelectedThangComponentConfig selectedThang, LevelComponent.CollidesID, (component) =>
@selectedExtantThang.collisionCategory = component.config.collisionCategory component.config ?= {}
component.config.collisionCategory = if component.config.collisionCategory is 'none' then 'ground' else 'none'
selectedThang.collisionCategory = component.config.collisionCategory
toggleThangsContainer: (e) -> toggleThangsContainer: (e) ->
$('#all-thangs').toggleClass('hide') $('#all-thangs').toggleClass('hide')

View file

@ -81,10 +81,10 @@ module.exports = class VerifierTest extends CocoClass
@updateCallback? state: 'running' @updateCallback? state: 'running'
processSingleGameResults: (e) -> processSingleGameResults: (e) ->
console.log(e)
@goals = e.goalStates @goals = e.goalStates
@frames = e.totalFrames @frames = e.totalFrames
@lastFrameHash = e.lastFrameHash @lastFrameHash = e.lastFrameHash
@simulationFrameRate = e.simulationFrameRate
@state = 'complete' @state = 'complete'
@updateCallback? state: @state @updateCallback? state: @state
@scheduleCleanup() @scheduleCleanup()
@ -92,6 +92,7 @@ module.exports = class VerifierTest extends CocoClass
isSuccessful: () -> isSuccessful: () ->
return false unless @solution? return false unless @solution?
return false unless @frames == @solution.frameCount or @options.dontCareAboutFrames return false unless @frames == @solution.frameCount or @options.dontCareAboutFrames
return false if @simulationFrameRate < 30
if @goals and @solution.goals if @goals and @solution.goals
for k of @goals for k of @goals
continue if not @solution.goals[k] continue if not @solution.goals[k]

View file

@ -72,6 +72,7 @@ module.exports =
fetchNextLevel: wrap (req, res) -> fetchNextLevel: wrap (req, res) ->
unless req.user? then return res.status(200).send({})
levelOriginal = req.params.levelOriginal levelOriginal = req.params.levelOriginal
unless database.isID(levelOriginal) then throw new errors.UnprocessableEntity('Invalid level original ObjectId') unless database.isID(levelOriginal) then throw new errors.UnprocessableEntity('Invalid level original ObjectId')
sessionID = req.params.sessionID sessionID = req.params.sessionID
@ -162,3 +163,9 @@ module.exports =
students: (user.toObject({req: req}) for user in users) students: (user.toObject({req: req}) for user in users)
prepaids: (prepaid.toObject({req: req}) for prepaid in prepaids) prepaids: (prepaid.toObject({req: req}) for prepaid in prepaids)
}) })
fetchNonHoc: wrap (req, res) ->
throw new errors.Unauthorized('You must be an administrator.') unless req.user?.isAdmin()
query = {$and: [{name: {$ne: 'Single Player'}}, {hourOfCode: {$ne: true}}]}
courseInstances = yield CourseInstance.find(query, { members: 1, ownerID: 1}).lean()
res.status(200).send(courseInstances)

View file

@ -80,6 +80,7 @@ module.exports.setup = (app) ->
app.get('/db/course/:handle', mw.rest.getByHandle(Course)) app.get('/db/course/:handle', mw.rest.getByHandle(Course))
app.get('/db/course/:handle/levels/:levelOriginal/next', mw.courses.fetchNextLevel) app.get('/db/course/:handle/levels/:levelOriginal/next', mw.courses.fetchNextLevel)
app.get('/db/course_instance/-/non-hoc', mw.auth.checkHasPermission(['admin']), mw.courseInstances.fetchNonHoc)
app.post('/db/course_instance/-/recent', mw.auth.checkHasPermission(['admin']), mw.courseInstances.fetchRecent) app.post('/db/course_instance/-/recent', mw.auth.checkHasPermission(['admin']), mw.courseInstances.fetchRecent)
app.get('/db/course_instance/:handle/levels/:levelOriginal/sessions/:sessionID/next', mw.courseInstances.fetchNextLevel) app.get('/db/course_instance/:handle/levels/:levelOriginal/sessions/:sessionID/next', mw.courseInstances.fetchNextLevel)
app.post('/db/course_instance/:handle/members', mw.auth.checkLoggedIn(), mw.courseInstances.addMembers) app.post('/db/course_instance/:handle/members', mw.auth.checkLoggedIn(), mw.courseInstances.addMembers)

View file

@ -96,24 +96,6 @@ describe 'Camera (Surface point of view)', ->
checkConversionsFromWorldPos wop, cam checkConversionsFromWorldPos wop, cam
checkCameraPos cam, wop checkCameraPos cam, wop
it 'works at 90 degrees', ->
cam = new Camera {attr: (attr) -> 100}, Math.PI / 2
expect(cam.x2y).toBeCloseTo 1
expect(cam.x2z).toBeGreaterThan 9001
expect(cam.z2y).toBeCloseTo 0
it 'works at 0 degrees', ->
cam = new Camera {attr: (attr) -> 100}, 0
expect(cam.x2y).toBeGreaterThan 9001
expect(cam.x2z).toBeCloseTo 1
expect(cam.z2y).toBeGreaterThan 9001
it 'works at 45 degrees', ->
cam = new Camera {attr: (attr) -> 100}, Math.PI / 4
expect(cam.x2y).toBeCloseTo Math.sqrt(2)
expect(cam.x2z).toBeCloseTo Math.sqrt(2)
expect(cam.z2y).toBeCloseTo 1
it 'works at default angle of asin(0.75) ~= 48.9 degrees', -> it 'works at default angle of asin(0.75) ~= 48.9 degrees', ->
cam = new Camera {attr: (attr) -> 100}, null cam = new Camera {attr: (attr) -> 100}, null
angle = Math.asin(3 / 4) angle = Math.asin(3 / 4)

View file

@ -2,6 +2,7 @@ LankBoss = require 'lib/surface/LankBoss'
Camera = require 'lib/surface/Camera' Camera = require 'lib/surface/Camera'
World = require 'lib/world/world' World = require 'lib/world/world'
ThangType = require 'models/ThangType' ThangType = require 'models/ThangType'
GameUIState = require 'models/GameUIState'
treeData = require 'test/app/fixtures/tree1.thang.type' treeData = require 'test/app/fixtures/tree1.thang.type'
munchkinData = require 'test/app/fixtures/ogre-munchkin-m.thang.type' munchkinData = require 'test/app/fixtures/ogre-munchkin-m.thang.type'
@ -53,6 +54,7 @@ describe 'LankBoss', ->
surfaceTextLayer: new createjs.Container() surfaceTextLayer: new createjs.Container()
world: world world: world
thangTypes: thangTypes thangTypes: thangTypes
gameUIState: new GameUIState()
} }
window.lankBoss = lankBoss = new LankBoss(options) window.lankBoss = lankBoss = new LankBoss(options)