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.ended)
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 diff = t1 - self.t0;
if(self.world.headless)
return console.log('Headless simulation completed in ' + diff + 'ms.');
var goalStates = self.goalManager.getGoalStates();
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 serialized;
@ -469,7 +470,7 @@ self.onWorldLoaded = function onWorldLoaded() {
if(worldEnded) {
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();
var goalStates = self.goalManager.getGoalStates();
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) {

View file

@ -370,3 +370,103 @@ module.exports.findNextLevel = (levels, currentIndex, needsPractice) ->
module.exports.needsPractice = (playtime=0, threshold=2) ->
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
when 'end-load-frames'
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'
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
when 'non-user-code-problem'
@ -125,11 +124,12 @@ module.exports = class Angel extends CocoClass
else
@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
event = goalStates: goalStates, preload: preload, overallStatus: overallStatus
event = goalStates: goalStates, preload: preload ? false, overallStatus: overallStatus
event.totalFrames = totalFrames if totalFrames?
event.lastFrameHash = lastFrameHash if lastFrameHash?
event.simulationFrameRate = simulationFrameRate if simulationFrameRate?
@publishGodEvent 'goals-calculated', event
@finishWork() if @shared.headless
@ -306,7 +306,8 @@ module.exports = class Angel extends CocoClass
work.world.goalManager.worldGenerationEnded() if work.world.ended
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
serialized = world.serialize()

View file

@ -1,4 +1,5 @@
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.
@ -44,12 +45,17 @@ module.exports = class Camera extends CocoClass
'camera:zoom-out': 'onZoomOut'
'camera:zoom-to': 'onZoomTo'
'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()
@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)
@canvasHeight = parseInt(@canvas.attr('height'), 10)
@offset = {x: 0, y: 0}
@ -155,8 +161,27 @@ module.exports = class Camera extends CocoClass
onZoomIn: (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) ->
return unless e.canvas is @canvas
ratio = 1 + 0.05 * Math.sqrt(Math.abs(e.deltaY))
ratio = 1 / ratio if e.deltaY > 0
newZoom = @zoom * ratio
@ -174,22 +199,6 @@ module.exports = class Camera extends CocoClass
target = @target
@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: ->
@setBounds(@firstBounds, false)

View file

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

View file

@ -25,10 +25,11 @@ module.exports = class LankBoss extends CocoClass
'surface:flag-appeared': 'onFlagAppeared'
'surface:remove-selected-flag': 'onRemoveSelectedFlag'
constructor: (@options) ->
constructor: (@options={}) ->
super()
@handleEvents = @options.handleEvents
@gameUIState = @options.gameUIState
@dragged = 0
@options ?= {}
@camera = @options.camera
@webGLStage = @options.webGLStage
@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
@createLayers()
@pendingFlags = []
if not @handleEvents
@listenTo @gameUIState, 'change:selected', @onChangeSelected
destroy: ->
@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'
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) ->
return if e.debug is @debug
@ -256,6 +268,7 @@ module.exports = class LankBoss extends CocoClass
@dragged += 1
onLankMouseUp: (e) ->
return unless @handleEvents
return if key.shift #and @options.choosing
return @dragged = 0 if @dragged > 3
@dragged = 0
@ -264,9 +277,27 @@ module.exports = class LankBoss extends CocoClass
@selectLank e, lank
onStageMouseDown: (e) ->
return unless @handleEvents
return if key.shift #and @options.choosing
@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) ->
return @willSelectThang = [thangID, spellName] unless @lanks[thangID]
@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
worldPos = lank?.thang?.pos
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
@camera.zoomTo(lank?.sprite or @camera.worldToSurface(worldPos), @camera.zoom, 1000, true)
if @handleEvents
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
if lank isnt @selectedLank
@selectedLank?.selected = false

View file

@ -18,6 +18,7 @@ LankBoss = require './LankBoss'
PointChooser = require './PointChooser'
RegionChooser = require './RegionChooser'
MusicPlayer = require './MusicPlayer'
GameUIState = require 'models/GameUIState'
resizeDelay = 500 # At least as much as $level-resize-transition-time.
@ -87,6 +88,10 @@ module.exports = Surface = class Surface extends CocoClass
@normalLayers = []
@options = _.clone(@defaults)
@options = _.extend(@options, givenOptions) if givenOptions
@handleEvents = @options.handleEvents ? true
@gameUIState = @options.gameUIState or new GameUIState({
canDragCamera: true
})
@initEasel()
@initAudio()
@onResize = _.debounce @onResize, resizeDelay
@ -98,7 +103,7 @@ module.exports = Surface = class Surface extends CocoClass
@normalStage = new createjs.Stage(@normalCanvas[0])
@webGLStage = new createjs.SpriteStage(@webGLCanvas[0])
@normalStage.nextStage = @webGLStage
@camera = new Camera @webGLCanvas
@camera = new Camera(@webGLCanvas, { @gameUIState, @handleEvents })
AudioPlayer.camera = @camera unless @options.choosing
@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
@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
@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.
@ -121,7 +138,7 @@ module.exports = Surface = class Surface extends CocoClass
@webGLStage.enableMouseOver(10)
@webGLStage.addEventListener 'stagemousemove', @onMouseMove
@webGLStage.addEventListener 'stagemousedown', @onMouseDown
@webGLCanvas[0].addEventListener 'mouseup', @onMouseUp
@webGLStage.addEventListener 'stagemouseup', @onMouseUp
@webGLCanvas.on 'mousewheel', @onMouseWheel
@hookUpChooseControls() if @options.choosing # TODO: figure this stuff out
createjs.Ticker.timingMode = createjs.Ticker.RAF_SYNCHED
@ -222,8 +239,9 @@ module.exports = Surface = class Surface extends CocoClass
updateState: (frameChanged) ->
# 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
@camera.zoomTo @heroLank.sprite, @camera.zoom, 750
if @handleEvents
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
@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
@ -371,7 +389,8 @@ module.exports = Surface = class Surface extends CocoClass
target = null
@camera.setBounds e.bounds if e.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) ->
if @ended
@ -476,6 +495,7 @@ module.exports = Surface = class Surface extends CocoClass
@mouseScreenPos = {x: e.stageX, y: e.stageY}
return if @disabled
Backbone.Mediator.publish 'surface:mouse-moved', x: e.stageX, y: e.stageY
@gameUIState.trigger('surface:stage-mouse-move', { originalEvent: e })
onMouseDown: (e) =>
return if @disabled
@ -484,16 +504,19 @@ module.exports = Surface = class Surface extends CocoClass
onBackground = not @webGLStage._getObjectsUnderPoint(e.stageX, e.stageY, null, true)
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 'tome:focus-editor', {}
@gameUIState.trigger('surface:stage-mouse-down', event)
@mouseIsDown = true
onMouseUp: (e) =>
return if @disabled
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', {}
@gameUIState.trigger('surface:stage-mouse-up', event)
@mouseIsDown = false
onMouseWheel: (e) =>
@ -506,6 +529,7 @@ module.exports = Surface = class Surface extends CocoClass
canvas: @webGLCanvas
event.screenPos = @mouseScreenPos if @mouseScreenPos
Backbone.Mediator.publish 'surface:mouse-scrolled', event unless @disabled
@gameUIState.trigger('surface:mouse-scrolled', event)
#- Canvas callbacks
@ -585,8 +609,9 @@ module.exports = Surface = class Surface extends CocoClass
@onResize()
_.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'
if @previousCameraZoom
@camera.zoomTo @camera.newTarget or @camera.target, @previousCameraZoom, 3000
if @handleEvents
if @previousCameraZoom
@camera.zoomTo @camera.newTarget or @camera.target, @previousCameraZoom, 3000
onFlagColorSelected: (e) ->
@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_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_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."
campaign: "Verhaallijn"
for_beginners: "Voor Beginners"
@ -16,7 +16,7 @@ module.exports = nativeDescription: "Nederlands (Nederland)", englishDescription
new_home:
slogan: "Het meest uitdagende spel om mee te leren programmeren."
classroom_edition: "Klas versie:"
classroom_edition: "Klasversie:"
learn_to_code: "Leer programmeren:"
teacher: "Leraar"
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 "
motivating: "motiveert"
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"
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."
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."
@ -1288,11 +1288,11 @@ module.exports = nativeDescription: "Nederlands (Nederland)", englishDescription
avg_student_exp_varied: "Verschilt enorm per leerling"
student_age_range_label: "Leeftijdscategorie leerlingen"
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"
create_class: "Maak klas aan"
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_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"
current_classes: "Huidige 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"
archive_class: "archiveer klas"
unarchive_class: "dearchiveer klas"
@ -1372,9 +1372,9 @@ module.exports = nativeDescription: "Nederlands (Nederland)", englishDescription
purchased: "Aanschaffing Voltooid!"
purchase_now: "Nu Aanschaffen"
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_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_3: "Zodra een leerling is ingeschreven zal deze toegang hebben tot alle inhoud van de cursus."
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: "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 heeft hij/zij toegang tot alle inhoud."
bulk_pricing_blurb: "Aanschaffen voor meer dan 25 leerlingen? Neem contact met ons op."
total_unenrolled: "Totaal aantal niet ingeschreven"
export_student_progress: "Exporteer Voortgang Leerlingen (CSV bestand)"

View file

@ -38,7 +38,7 @@ module.exports = nativeDescription: "русский", englishDescription: "Russi
accessible_to: "Доступно для"
everyone: "каждого"
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."
# why_games: "Why is learning through games important?"
# 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]}
totalFrames: {type: ['integer', 'undefined']}
lastFrameHash: {type: ['number', 'undefined']}
simulationFrameRate: {type: ['number', 'undefined']}
'god:world-load-progress-changed': c.object {required: ['progress', 'god']},
god: {type: 'object'}

View file

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

View file

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

View file

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

View file

@ -11,6 +11,7 @@ Thang = require 'lib/world/thang'
LevelThangEditView = require './LevelThangEditView'
ComponentsCollection = require 'collections/ComponentsCollection'
require 'vendor/treema'
GameUIState = require 'models/GameUIState'
# Moving the screen while dragging thangs constants
MOVE_MARGIN = 0.15
@ -29,7 +30,6 @@ module.exports = class ThangsTabView extends CocoView
template: thangs_template
subscriptions:
'surface:sprite-selected': 'onExtantThangSelected'
'surface:mouse-moved': 'onSurfaceMouseMoved'
'surface:mouse-over': 'onSurfaceMouseOver'
'surface:mouse-out': 'onSurfaceMouseOut'
@ -39,7 +39,6 @@ module.exports = class ThangsTabView extends CocoView
'editor:view-switched': 'onViewSwitched'
'sprite:dragged': 'onSpriteDragged'
'sprite:mouse-up': 'onSpriteMouseUp'
'sprite:mouse-down': 'onSpriteMouseDown'
'sprite:double-clicked': 'onSpriteDoubleClicked'
'surface:stage-mouse-down': 'onStageMouseDown'
'surface:stage-mouse-up': 'onStageMouseUp'
@ -78,12 +77,17 @@ module.exports = class ThangsTabView extends CocoView
constructor: (options) ->
super options
@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
@thangTypes = @supermodel.loadCollection(new ThangTypeSearchCollection(), 'thangs').model
# just loading all Components for now: https://github.com/codecombat/codecombat/issues/405
@componentCollection = @supermodel.loadCollection(new ComponentsCollection(), 'components').load()
@level = options.level
@onThangsChanged = _.debounce(@onThangsChanged)
$(document).bind 'contextmenu', @preventDefaultContextMenu
@ -203,7 +207,7 @@ module.exports = class ThangsTabView extends CocoView
initSurface: ->
webGLCanvas = $('canvas#webgl-surface', @$el)
normalCanvas = $('canvas#normal-surface', @$el)
@surface = new Surface @world, normalCanvas, webGLCanvas, {
@surface = new Surface(@world, normalCanvas, webGLCanvas, {
paths: false
coords: true
grid: true
@ -212,7 +216,9 @@ module.exports = class ThangsTabView extends CocoView
showInvisible: true
frameRate: 15
levelType: @level.get 'type', true
}
@gameUIState
handleEvents: false
})
@surface.playing = false
@surface.setWorld @world
@surface.lankBoss.suppressSelectionSounds = true
@ -240,38 +246,79 @@ module.exports = class ThangsTabView extends CocoView
@selectAddThang null, true
@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) ->
return unless @addThangLank?.thangType.get('kind') is 'Wall'
@surface.camera.dragDisabled = true
@paintingWalls = true
# initial values for a mouse click lifecycle
@dragged = 0
@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) ->
if @paintingWalls
# 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 {}
@paintingWalls = false
$('#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) ->
return unless @selectedExtantThang and e.thang?.id is @selectedExtantThang?.id
@dragged = true
@surface.camera.dragDisabled = true
selected = @gameUIState.get('selected')
return unless _.any(selected) and @dragged > 10
@willUnselectSprite = false
{stageX, stageY} = e.originalEvent
# move the one under the mouse
lastSelected = _.last(selected)
cap = @surface.camera.screenToCanvas x: stageX, y: stageY
wop = @surface.camera.canvasToWorld cap
wop.z = @selectedExtantThang.depth / 2
@adjustThangPos @selectedExtantLank, @selectedExtantThang, wop
wop.z = lastSelected.thang.depth / 2
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]
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
@ -279,24 +326,30 @@ module.exports = class ThangsTabView extends CocoView
@calculateMovement(cap.x / w, cap.y / h, w / h)
onSpriteMouseUp: (e) ->
clearTimeout @backgroundAddClickTimeout
@surface.camera.unlock()
if e.originalEvent.nativeEvent.button == 2 and @selectedExtantThang
selected = @gameUIState.get('selected')
if e.originalEvent.nativeEvent.button == 2 and _.any(selected)
@onSpriteContextMenu e
clearInterval(@movementInterval) if @movementInterval?
@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})
path = "#{@pathForThang(thang)}/components/original=#{LevelComponent.PhysicalID}"
physical = @thangsTreema.get path
return 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
return unless _.any(selected)
for singleSelected in selected
pos = singleSelected.thang.pos
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) ->
return unless e.thang and not @dragged
return if @dragged > 10
return unless e.thang
@editThang thangID: e.thang.id
onRandomTerrainGenerated: (e) ->
@ -320,35 +373,21 @@ module.exports = class ThangsTabView extends CocoView
@onThangsChanged()
@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
onExtantThangSelected: (e) ->
@selectedExtantLank?.setNameLabel? null unless @selectedExtantLank is e.sprite
@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)
previousSprite?.setNameLabel?(null) unless previousSprite is sprite
if thang and not (@addThangLank and @addThangType.get('name') in overlappableThangTypeNames)
# We clicked on a Thang (or its Treema), so select the Thang
@selectAddThang null, true
@selectAddThang(null, true)
@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
@selectedExtantLank.setNameLabel @selectedExtantLank.thangType.get('name') + ': ' + @selectedExtantThang.id
@selectedExtantLank.updateLabels()
@selectedExtantLank.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()
sprite.setNameLabel(sprite.thangType.get('name') + ': ' + thang.id)
sprite.updateLabels()
sprite.updateMarks()
justAdded: -> @lastAddTime and (new Date() - @lastAddTime) < 150
@ -482,11 +521,14 @@ module.exports = class ThangsTabView extends CocoView
deleteSelectedExtantThang: (e) =>
return if $(e.target).hasClass 'treema-node'
return unless @selectedExtantThang
thang = @getThangByID(@selectedExtantThang.id)
@thangsTreema.delete(@pathForThang(thang))
@deleteEmptyTreema(thang)
Thang.resetThangIDs() # TODO: find some way to do this when we delete from treema, too
selected = @gameUIState.get('selected')
return unless _.any(selected)
for singleSelected in selected
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)->
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
# update selection, since the thangs have been remade
if @selectedExtantThang
@selectedExtantLank = @surface.lankBoss.lanks[@selectedExtantThang.id]
@selectedExtantThang = @selectedExtantLank?.thang
selected = @gameUIState.get('selected')
if _.any(selected)
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
onTreemaThangSelected: (e, selectedTreemas) =>
selectedThangID = _.last(selectedTreemas)?.data.id
if selectedThangID isnt @selectedExtantThang?.id
@surface.lankBoss.selectThang selectedThangID, null, true
selectedThangTreemas = _.filter(selectedTreemas, (t) -> t instanceof ThangNode)
thangIDs = (node.data.id for node in selectedThangTreemas)
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) =>
nativeEvent = e.originalEvent.nativeEvent
return if nativeEvent and (nativeEvent.ctrlKey or nativeEvent.metaKey)
id = treema?.data?.id
@editThang thangID: id if id
@ -655,7 +706,8 @@ module.exports = class ThangsTabView extends CocoView
onDuplicateClicked: (e) ->
$('#contextmenu').hide()
@selectAddThangType @selectedExtantThang.spriteName, @selectedExtantThang
selected = _.last(@gameUIState.get('selected'))
@selectAddThangType(selected.thang.spriteName, selected.thang)
onClickRotationButton: (e) ->
$('#contextmenu').hide()
@ -667,7 +719,8 @@ module.exports = class ThangsTabView extends CocoView
@hush = true
thangData = @getThangByID thang.id
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
thangData.components.push component
modificationFunction component
@ -682,34 +735,44 @@ module.exports = class ThangsTabView extends CocoView
lank.setDebug true
rotateSelectedThangTo: (radians) ->
@modifySelectedThangComponentConfig @selectedExtantThang, LevelComponent.PhysicalID, (component) =>
component.config.rotation = radians
@selectedExtantThang.rotation = component.config.rotation
for singleSelected in @gameUIState.get('selected')
selectedThang = singleSelected.thang
@modifySelectedThangComponentConfig selectedThang, LevelComponent.PhysicalID, (component) =>
component.config.rotation = radians
selectedThang.rotation = component.config.rotation
rotateSelectedThangBy: (radians) ->
@modifySelectedThangComponentConfig @selectedExtantThang, LevelComponent.PhysicalID, (component) =>
component.config.rotation = ((component.config.rotation ? 0) + radians) % (2 * Math.PI)
@selectedExtantThang.rotation = component.config.rotation
for singleSelected in @gameUIState.get('selected')
selectedThang = singleSelected.thang
@modifySelectedThangComponentConfig selectedThang, LevelComponent.PhysicalID, (component) =>
component.config.rotation = ((component.config.rotation ? 0) + radians) % (2 * Math.PI)
selectedThang.rotation = component.config.rotation
moveSelectedThangBy: (xDir, yDir) ->
@modifySelectedThangComponentConfig @selectedExtantThang, LevelComponent.PhysicalID, (component) =>
component.config.pos.x += 0.5 * xDir
component.config.pos.y += 0.5 * yDir
@selectedExtantThang.pos.x = component.config.pos.x
@selectedExtantThang.pos.y = component.config.pos.y
for singleSelected in @gameUIState.get('selected')
selectedThang = singleSelected.thang
@modifySelectedThangComponentConfig selectedThang, LevelComponent.PhysicalID, (component) =>
component.config.pos.x += 0.5 * xDir
component.config.pos.y += 0.5 * yDir
selectedThang.pos.x = component.config.pos.x
selectedThang.pos.y = component.config.pos.y
resizeSelectedThangBy: (xDir, yDir) ->
@modifySelectedThangComponentConfig @selectedExtantThang, LevelComponent.PhysicalID, (component) =>
component.config.width = (component.config.width ? 4) + 0.5 * xDir
component.config.height = (component.config.height ? 4) + 0.5 * yDir
@selectedExtantThang.width = component.config.width
@selectedExtantThang.height = component.config.height
for singleSelected in @gameUIState.get('selected')
selectedThang = singleSelected.thang
@modifySelectedThangComponentConfig selectedThang, LevelComponent.PhysicalID, (component) =>
component.config.width = (component.config.width ? 4) + 0.5 * xDir
component.config.height = (component.config.height ? 4) + 0.5 * yDir
selectedThang.width = component.config.width
selectedThang.height = component.config.height
toggleSelectedThangCollision: ->
@modifySelectedThangComponentConfig @selectedExtantThang, LevelComponent.CollidesID, (component) =>
component.config ?= {}
component.config.collisionCategory = if component.config.collisionCategory is 'none' then 'ground' else 'none'
@selectedExtantThang.collisionCategory = component.config.collisionCategory
for singleSelected in @gameUIState.get('selected')
selectedThang = singleSelected.thang
@modifySelectedThangComponentConfig selectedThang, LevelComponent.CollidesID, (component) =>
component.config ?= {}
component.config.collisionCategory = if component.config.collisionCategory is 'none' then 'ground' else 'none'
selectedThang.collisionCategory = component.config.collisionCategory
toggleThangsContainer: (e) ->
$('#all-thangs').toggleClass('hide')

View file

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

View file

@ -72,6 +72,7 @@ module.exports =
fetchNextLevel: wrap (req, res) ->
unless req.user? then return res.status(200).send({})
levelOriginal = req.params.levelOriginal
unless database.isID(levelOriginal) then throw new errors.UnprocessableEntity('Invalid level original ObjectId')
sessionID = req.params.sessionID
@ -162,3 +163,9 @@ module.exports =
students: (user.toObject({req: req}) for user in users)
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/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.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)

View file

@ -96,24 +96,6 @@ describe 'Camera (Surface point of view)', ->
checkConversionsFromWorldPos wop, cam
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', ->
cam = new Camera {attr: (attr) -> 100}, null
angle = Math.asin(3 / 4)

View file

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