diff --git a/app/assets/javascripts/workers/aether_worker.js b/app/assets/javascripts/workers/aether_worker.js index 65badb459..a75469a29 100644 --- a/app/assets/javascripts/workers/aether_worker.js +++ b/app/assets/javascripts/workers/aether_worker.js @@ -2,7 +2,7 @@ var window = self; var Global = self; importScripts("/javascripts/lodash.js", "/javascripts/aether.js"); -console.log("imported scripts!"); +console.log("Aether Tome worker has finished importing scripts."); var aethers = {}; var createAether = function (spellKey, options) @@ -96,4 +96,4 @@ self.addEventListener('message', function(e) { var returnObject = {"message":message, "function":"none"}; self.postMessage(JSON.stringify(returnObject)); } -}, false); \ No newline at end of file +}, false); diff --git a/app/collections/CocoCollection.coffee b/app/collections/CocoCollection.coffee index 64074a327..817b0700f 100644 --- a/app/collections/CocoCollection.coffee +++ b/app/collections/CocoCollection.coffee @@ -8,4 +8,9 @@ module.exports = class CocoCollection extends Backbone.Collection model.loaded = true for model in @models getURL: -> - return if _.isString @url then @url else @url() \ No newline at end of file + return if _.isString @url then @url else @url() + + fetch: -> + @jqxhr = super(arguments...) + @loading = true + @jqxhr diff --git a/app/collections/ThangNamesCollection.coffee b/app/collections/ThangNamesCollection.coffee new file mode 100644 index 000000000..414628d84 --- /dev/null +++ b/app/collections/ThangNamesCollection.coffee @@ -0,0 +1,14 @@ +ThangType = require 'models/ThangType' +CocoCollection = require 'collections/CocoCollection' + +module.exports = class ThangNamesCollection extends CocoCollection + url: '/db/thang.type/names' + model: ThangType + isCachable: false + + constructor: (@ids) -> super() + + fetch: (options) -> + options ?= {} + _.extend options, {type:'POST', data:{ids:@ids}} + super(options) diff --git a/app/lib/LevelLoader.coffee b/app/lib/LevelLoader.coffee index 5d539eae4..61e0a7918 100644 --- a/app/lib/LevelLoader.coffee +++ b/app/lib/LevelLoader.coffee @@ -4,6 +4,7 @@ LevelSystem = require 'models/LevelSystem' Article = require 'models/Article' LevelSession = require 'models/LevelSession' ThangType = require 'models/ThangType' +ThangNamesCollection = require 'collections/ThangNamesCollection' CocoClass = require 'lib/CocoClass' AudioPlayer = require 'lib/AudioPlayer' @@ -21,8 +22,10 @@ World = require 'lib/world/world' module.exports = class LevelLoader extends CocoClass constructor: (options) -> + @t0 = new Date().getTime() super() @supermodel = options.supermodel + @supermodel.setMaxProgress 0.2 @levelID = options.levelID @sessionID = options.sessionID @opponentSessionID = options.opponentSessionID @@ -103,17 +106,18 @@ module.exports = class LevelLoader extends CocoClass objUniq = (array) -> _.uniq array, false, (arg) -> JSON.stringify(arg) - for thangID in _.uniq thangIDs - url = "/db/thang.type/#{thangID}/version" - url += "?project=true" if @headless and not @editorMode - res = @maybeLoadURL url, ThangType, 'thang' - @listenToOnce res.model, 'sync', @buildSpriteSheetsForThangType if res + worldNecessities = [] + + @thangIDs = _.uniq thangIDs + @thangNames = new ThangNamesCollection(@thangIDs) + worldNecessities.push @supermodel.loadCollection(@thangNames, 'thang_names') + for obj in objUniq componentVersions url = "/db/level.component/#{obj.original}/version/#{obj.majorVersion}" - @maybeLoadURL url, LevelComponent, 'component' + worldNecessities.push @maybeLoadURL(url, LevelComponent, 'component') for obj in objUniq systemVersions url = "/db/level.system/#{obj.original}/version/#{obj.majorVersion}" - @maybeLoadURL url, LevelSystem, 'system' + worldNecessities.push @maybeLoadURL(url, LevelSystem, 'system') for obj in objUniq articleVersions url = "/db/article/#{obj.original}/version/#{obj.majorVersion}" @maybeLoadURL url, Article, 'article' @@ -125,16 +129,51 @@ module.exports = class LevelLoader extends CocoClass wizard = ThangType.loadUniversalWizard() @supermodel.loadModel wizard, 'thang' + jqxhrs = (resource.jqxhr for resource in worldNecessities when resource?.jqxhr) + $.when(jqxhrs...).done(@onWorldNecessitiesLoaded) + + onWorldNecessitiesLoaded: => + @initWorld() + @supermodel.clearMaxProgress() + return if @headless and not @editorMode + thangsToLoad = _.uniq( (t.spriteName for t in @world.thangs) ) + nameModelTuples = ([thangType.get('name'), thangType] for thangType in @thangNames.models) + nameModelMap = _.zipObject nameModelTuples + @spriteSheetsToBuild = [] + + for thangTypeName in thangsToLoad + thangType = nameModelMap[thangTypeName] + thangType.fetch() + thangType = @supermodel.loadModel(thangType, 'thang').model + res = @supermodel.addSomethingResource "sprite_sheet", 5 + res.thangType = thangType + res.markLoading() + @spriteSheetsToBuild.push res + + @buildLoopInterval = setInterval @buildLoop, 5 + maybeLoadURL: (url, Model, resourceName) -> return if @supermodel.getModel(url) model = new Model().setURL url @supermodel.loadModel(model, resourceName) onSupermodelLoaded: -> + console.log 'SuperModel for Level loaded in', new Date().getTime() - @t0, 'ms' @loadLevelSounds() @denormalizeSession() app.tracker.updatePlayState(@level, @session) unless @headless - @initWorld() + + buildLoop: => + return if @lastBuilt and new Date().getTime() - @lastBuilt < 10 + return clearInterval @buildLoopInterval unless @spriteSheetsToBuild.length + + for spriteSheetResource, i in @spriteSheetsToBuild + if spriteSheetResource.thangType.loaded + @buildSpriteSheetsForThangType spriteSheetResource.thangType + @spriteSheetsToBuild.splice i, 1 + @lastBuilt = new Date().getTime() + spriteSheetResource.markLoaded() + return denormalizeSession: -> return if @headless or @sessionDenormalized or @spectateMode @@ -156,6 +195,10 @@ module.exports = class LevelLoader extends CocoClass buildSpriteSheetsForThangType: (thangType) -> return if @headless + # TODO: Finish making sure the supermodel loads the raster image before triggering load complete, and that the cocosprite has access to the asset. +# if f = thangType.get('raster') +# queue = new createjs.LoadQueue() +# queue.loadFile('/file/'+f) @grabThangTypeTeams() unless @thangTypeTeams for team in @thangTypeTeams[thangType.get('original')] ? [null] spriteOptions = {resolutionFactor: 4, async: false} @@ -198,6 +241,7 @@ module.exports = class LevelLoader extends CocoClass @world = new World() serializedLevel = @level.serialize(@supermodel) @world.loadFromLevel serializedLevel, false + console.log "World has been initialized from level loader." # Initial Sound Loading @@ -223,3 +267,7 @@ module.exports = class LevelLoader extends CocoClass # everything else sound wise is loaded as needed as worlds are generated progress: -> @supermodel.progress + + destroy: -> + clearInterval @buildLoopInterval if @buildLoopInterval + super() diff --git a/app/lib/simulator/Simulator.coffee b/app/lib/simulator/Simulator.coffee index ff549797f..e8b326b49 100644 --- a/app/lib/simulator/Simulator.coffee +++ b/app/lib/simulator/Simulator.coffee @@ -74,7 +74,7 @@ module.exports = class Simulator extends CocoClass return @supermodel ?= new SuperModel() - + @supermodel.resetProgress() @levelLoader = new LevelLoader supermodel: @supermodel, levelID: levelID, sessionID: @task.getFirstSessionID(), headless: true if @supermodel.finished() @simulateGame() diff --git a/app/lib/surface/CocoSprite.coffee b/app/lib/surface/CocoSprite.coffee index 225c6b308..f3a2327a2 100644 --- a/app/lib/surface/CocoSprite.coffee +++ b/app/lib/surface/CocoSprite.coffee @@ -70,7 +70,7 @@ module.exports = CocoSprite = class CocoSprite extends CocoClass @age = 0 @scaleFactor = @targetScaleFactor = 1 @displayObject = new createjs.Container() - if @thangType.get('actions') + if @thangType.isFullyLoaded() @setupSprite() else @stillLoading = true @@ -79,9 +79,29 @@ module.exports = CocoSprite = class CocoSprite extends CocoClass setupSprite: -> @stillLoading = false - @actions = @thangType.getActions() - @buildFromSpriteSheet @buildSpriteSheet() - @createMarks() + if @thangType.get('raster') + @isRaster = true + @setUpRasterImage() + @actions = {} + else + @actions = @thangType.getActions() + @buildFromSpriteSheet @buildSpriteSheet() + @createMarks() + + setUpRasterImage: -> + raster = @thangType.get('raster') + sprite = @imageObject = new createjs.Bitmap('/file/'+raster) + @displayObject.addChild(sprite) + @configureMouse() + @originalScaleX = sprite.scaleX + @originalScaleY = sprite.scaleY + @displayObject.sprite = @ + @displayObject.layerPriority = @thangType.get 'layerPriority' + @displayObject.name = @thang?.spriteName or @thangType.get 'name' + reg = @getOffset 'registration' + @imageObject.regX = -reg.x + @imageObject.regY = -reg.y + @updateScale() destroy: -> mark.destroy() for name, mark of @marks @@ -126,6 +146,7 @@ module.exports = CocoSprite = class CocoSprite extends CocoClass queueAction: (action) -> # The normal way to have an action play + return unless @thangType.isFullyLoaded() action = @actions[action] if _.isString(action) action ?= @actions.idle @actionQueue = [] @@ -143,6 +164,7 @@ module.exports = CocoSprite = class CocoSprite extends CocoClass @playAction(@actionQueue.splice(0,1)[0]) if @actionQueue.length playAction: (action) -> + return if @isRaster @currentAction = action return @hide() unless action.animation or action.container or action.relatedActions @show() @@ -245,15 +267,17 @@ module.exports = CocoSprite = class CocoSprite extends CocoClass @hasMoved = true updateScale: -> + return unless @imageObject if @thangType.get('matchWorldDimensions') and @thang if @thang.width isnt @lastThangWidth or @thang.height isnt @lastThangHeight - [@lastThangWidth, @lastThangHeight] = [@thang.width, @thang.height] bounds = @imageObject.getBounds() + return unless bounds # TODO: remove this because it's a bandaid over the image sometimes not being loaded @imageObject.scaleX = @thang.width * Camera.PPM / bounds.width @imageObject.scaleY = @thang.height * Camera.PPM * @options.camera.y2x / bounds.height unless @thang.spriteName is 'Beam' @imageObject.scaleX *= @thangType.get('scale') ? 1 @imageObject.scaleY *= @thangType.get('scale') ? 1 + [@lastThangWidth, @lastThangHeight] = [@thang.width, @thang.height] return scaleX = if @getActionProp 'flipX' then -1 else 1 scaleY = if @getActionProp 'flipY' then -1 else 1 @@ -270,6 +294,12 @@ module.exports = CocoSprite = class CocoSprite extends CocoClass angle = -angle if angle < 0 angle = 180 - angle if angle > 90 scaleX = 0.5 + 0.5 * (90 - angle) / 90 + + if @isRaster # scale is worked into building the sprite sheet for animations + scale = @thangType.get('scale') or 1 + scaleX *= scale + scaleY *= scale + scaleFactorX = @thang.scaleFactorX ? @scaleFactor scaleFactorY = @thang.scaleFactorY ? @scaleFactor @imageObject.scaleX = @originalScaleX * scaleX * scaleFactorX @@ -322,6 +352,7 @@ module.exports = CocoSprite = class CocoSprite extends CocoClass ################################################## updateAction: -> + return if @isRaster action = @determineAction() isDifferent = action isnt @currentRootAction or action is null if not action and @thang?.actionActivated and not @stopLogging @@ -443,10 +474,11 @@ module.exports = CocoSprite = class CocoSprite extends CocoClass def = x: 0, y: {registration: 0, torso: -50, mouth: -60, aboveHead: -100}[prop] pos = @getActionProp 'positions', prop, def pos = x: pos.x, y: pos.y - scale = @getActionProp 'scale', null, 1 - scale *= @options.resolutionFactor if prop is 'registration' - pos.x *= scale - pos.y *= scale + if not @isRaster + scale = @getActionProp 'scale', null, 1 + scale *= @options.resolutionFactor if prop is 'registration' + pos.x *= scale + pos.y *= scale if @thang and prop isnt 'registration' scaleFactor = @thang.scaleFactor ? 1 pos.x *= @thang.scaleFactorX ? scaleFactor @@ -658,7 +690,7 @@ module.exports = CocoSprite = class CocoSprite extends CocoClass z = @shadow.pos.z @shadow.pos = pos @shadow.pos.z = z - @imageObject.gotoAndPlay(endAnimation) + @imageObject.gotoAndPlay?(endAnimation) return @shadow.action = 'move' diff --git a/app/lib/surface/Layer.coffee b/app/lib/surface/Layer.coffee index a0f8d5ad4..1e438a2ea 100644 --- a/app/lib/surface/Layer.coffee +++ b/app/lib/surface/Layer.coffee @@ -101,4 +101,5 @@ module.exports = class Layer extends createjs.Container cache: -> return unless @children.length bounds = @getBounds() + return unless bounds super bounds.x, bounds.y, bounds.width, bounds.height, 2 diff --git a/app/lib/surface/Mark.coffee b/app/lib/surface/Mark.coffee index 8cc3bcfa7..a9b26972f 100644 --- a/app/lib/surface/Mark.coffee +++ b/app/lib/surface/Mark.coffee @@ -200,7 +200,7 @@ module.exports = class Mark extends CocoClass Backbone.Mediator.publish 'sprite:loaded' update: (pos=null) -> - return false unless @on and @mark + return false unless @on and @mark and @sprite?.thangType.isFullyLoaded() @mark.visible = not @hidden @updatePosition pos @updateRotation() @@ -242,7 +242,7 @@ module.exports = class Mark extends CocoClass oldMark.parent.removeChild oldMark return unless @name in ["selection", "target", "repair", "highlight"] scale = 0.5 - if @sprite + if @sprite?.imageObject size = @sprite.getAverageDimension() size += 60 if @name is 'selection' size += 60 if @name is 'repair' diff --git a/app/lib/surface/SpriteBoss.coffee b/app/lib/surface/SpriteBoss.coffee index df3642e83..bda6e1992 100644 --- a/app/lib/surface/SpriteBoss.coffee +++ b/app/lib/surface/SpriteBoss.coffee @@ -47,7 +47,7 @@ module.exports = class SpriteBoss extends CocoClass toString: -> "<SpriteBoss: #{@spriteArray.length} sprites>" thangTypeFor: (type) -> - _.find @options.thangTypes, (m) -> m.get('original') is type or m.get('name') is type + _.find @options.thangTypes, (m) -> m.get('actions') and m.get('original') is type or m.get('name') is type createLayers: -> @spriteLayers = {} @@ -144,7 +144,11 @@ module.exports = class SpriteBoss extends CocoClass addThangToSprites: (thang, layer=null) -> return console.warn 'Tried to add Thang to the surface it already has:', thang.id if @sprites[thang.id] - thangType = _.find @options.thangTypes, (m) -> m.get('name') is thang.spriteName + thangType = _.find @options.thangTypes, (m) -> + return false unless m.get('actions') or m.get('raster') + return m.get('name') is thang.spriteName + thangType ?= _.find @options.thangTypes, (m) -> return m.get('name') is thang.spriteName + options = @createSpriteOptions thang: thang options.resolutionFactor = if thangType.get('kind') is 'Floor' then 2 else 4 sprite = new CocoSprite thangType, options @@ -196,6 +200,8 @@ module.exports = class SpriteBoss extends CocoClass cache: (update=false) -> return if @cached and not update wallSprites = (sprite for sprite in @spriteArray when sprite.thangType?.get('name').search(/(dungeon|indoor).wall/i) isnt -1) + unless _.all (s.thangType.isFullyLoaded() for s in wallSprites) + return walls = (sprite.thang for sprite in wallSprites) @world.calculateBounds() wallGrid = new Grid walls, @world.size()... diff --git a/app/lib/surface/Surface.coffee b/app/lib/surface/Surface.coffee index 506626c51..62f0901b2 100644 --- a/app/lib/surface/Surface.coffee +++ b/app/lib/surface/Surface.coffee @@ -84,6 +84,8 @@ module.exports = Surface = class Surface extends CocoClass @initAudio() @onResize = _.debounce @onResize, 500 $(window).on 'resize', @onResize + if @world.ended + _.defer => @setWorld @world destroy: -> @dead = true diff --git a/app/locale/en.coffee b/app/locale/en.coffee index b8c3d7707..5c10f0a1c 100644 --- a/app/locale/en.coffee +++ b/app/locale/en.coffee @@ -699,6 +699,7 @@ user_schema: "User Schema" user_profile: "User Profile" patches: "Patches" + patched_model: "Source Document" model: "Model" system: "System" component: "Component" @@ -709,10 +710,12 @@ opponent_session: "Opponent Session" article: "Article" user_names: "User Names" + thang_names: "Thang Names" files: "Files" top_simulators: "Top Simulators" source_document: "Source Document" document: "Document" # note to diplomats: not a physical document, a document in MongoDB, ie a record in a database + sprite_sheet: "Sprite Sheet" delta: added: "Added" diff --git a/app/models/CocoModel.coffee b/app/models/CocoModel.coffee index 75d0172dd..35f0318ed 100644 --- a/app/models/CocoModel.coffee +++ b/app/models/CocoModel.coffee @@ -37,6 +37,8 @@ class CocoModel extends Backbone.Model @loading = false @markToRevert() @loadFromBackup() + + getNormalizedURL: -> "#{@urlRoot}/#{@id}" set: -> res = super(arguments...) @@ -76,9 +78,9 @@ class CocoModel extends Backbone.Model return super attrs, options fetch: -> - res = super(arguments...) + @jqxhr = super(arguments...) @loading = true - res + @jqxhr markToRevert: -> if @type() is 'ThangType' diff --git a/app/models/SuperModel.coffee b/app/models/SuperModel.coffee index 465e2b17b..cbfc1d2d2 100644 --- a/app/models/SuperModel.coffee +++ b/app/models/SuperModel.coffee @@ -5,6 +5,7 @@ module.exports = class SuperModel extends Backbone.Model @progress = 0 @resources = {} @rid = 0 + @maxProgress = 1 @models = {} @collections = {} @@ -19,7 +20,6 @@ module.exports = class SuperModel extends Backbone.Model loadModel: (model, name, fetchOptions, value=1) -> cachedModel = @getModelByURL(model.getURL()) if cachedModel - console.debug 'Model cache hit', cachedModel.getURL(), 'already loaded', cachedModel.loaded if cachedModel.loaded res = @addModelResource(cachedModel, name, fetchOptions, 0) res.markLoaded() @@ -96,7 +96,7 @@ module.exports = class SuperModel extends Backbone.Model @registerCollection(collection) registerCollection: (collection) -> - @collections[collection.getURL()] = collection + @collections[collection.getURL()] = collection if collection.isCachable # consolidate models for model, i in collection.models cachedModel = @getModelByURL(model.getURL()) @@ -141,7 +141,7 @@ module.exports = class SuperModel extends Backbone.Model @listenToOnce(resource, 'loaded', @onResourceLoaded) @listenTo(resource, 'failed', @onResourceFailed) @denom += value - @updateProgress() if @denom + _.defer @updateProgress if @denom onResourceLoaded: (r) -> @num += r.value @@ -155,11 +155,18 @@ module.exports = class SuperModel extends Backbone.Model # a bunch of things load all at once. # So make sure we only emit events if @progress has changed. newProg = if @denom then @num / @denom else 1 - return if @progress is newProg + newProg = Math.min @maxProgress, newProg + return if @progress >= newProg @progress = newProg @trigger('update-progress', @progress) @trigger('loaded-all') if @finished() - + + setMaxProgress: (@maxProgress) -> + resetProgress: -> @progress = 0 + clearMaxProgress: -> + @maxProgress = 1 + _.defer @updateProgress + getProgress: -> return @progress getResource: (rid) -> @@ -202,6 +209,7 @@ class ModelResource extends Resource super(name, value) @model = modelOrCollection @fetchOptions = fetchOptions + @jqxhr = @model.jqxhr load: -> @markLoading() diff --git a/app/models/ThangType.coffee b/app/models/ThangType.coffee index 21cd1fadd..345bc0264 100644 --- a/app/models/ThangType.coffee +++ b/app/models/ThangType.coffee @@ -26,12 +26,18 @@ module.exports = class ThangType extends CocoModel @buildActions() @spriteSheets = {} @building = {} + + isFullyLoaded: -> + # TODO: Come up with a better way to identify when the model doesn't have everything needed to build the sprite. ie when it's a projection without all the required data. + return @get('actions') or @get('raster') # needs one of these two things getActions: -> + return {} unless @isFullyLoaded() return @actions or @buildActions() buildActions: -> - @actions = $.extend(true, {}, @get('actions') or {}) + return null unless @isFullyLoaded() + @actions = $.extend(true, {}, @get('actions')) for name, action of @actions action.name = name for relatedName, relatedAction of action.relatedActions ? {} @@ -52,9 +58,12 @@ module.exports = class ThangType extends CocoModel options buildSpriteSheet: (options) -> + return false unless @isFullyLoaded() @options = @fillOptions options key = @spriteSheetKey(@options) + if ss = @spriteSheets[key] then return ss return if @building[key] + @t0 = new Date().getTime() @initBuild(options) @addGeneralFrames() unless @options.portraitOnly @addPortrait() @@ -144,9 +153,8 @@ module.exports = class ThangType extends CocoModel @builder.buildAsync() unless buildQueue.length > 1 @builder.on 'complete', @onBuildSpriteSheetComplete, @, true, key return true - t0 = new Date() spriteSheet = @builder.build() - console.warn "Built #{@get('name')} in #{new Date() - t0}ms on main thread." + console.debug "Built #{@get('name')} in #{new Date().getTime() - @t0}ms." @spriteSheets[key] = spriteSheet delete @building[key] spriteSheet @@ -180,6 +188,7 @@ module.exports = class ThangType extends CocoModel stage?.toDataURL() getPortraitStage: (spriteOptionsOrKey, size=100) -> + return unless @isFullyLoaded() key = spriteOptionsOrKey key = if _.isString(key) then key else @spriteSheetKey(@fillOptions(key)) spriteSheet = @spriteSheets[key] @@ -210,8 +219,8 @@ module.exports = class ThangType extends CocoModel @tick = null stage - uploadGenericPortrait: (callback) -> - src = @getPortraitSource() + uploadGenericPortrait: (callback, src) -> + src ?= @getPortraitSource() return callback?() unless src src = src.replace('data:image/png;base64,', '').replace(/\ /g, '+') body = diff --git a/app/schemas/models/thang_type.coffee b/app/schemas/models/thang_type.coffee index eb78c1c11..37624c180 100644 --- a/app/schemas/models/thang_type.coffee +++ b/app/schemas/models/thang_type.coffee @@ -123,6 +123,7 @@ _.extend ThangTypeSchema.properties, title: 'Scale' type: 'number' positions: PositionsSchema + raster: { type: 'string', format: 'image-file', title: 'Raster Image' } colorGroups: c.object title: 'Color Groups' additionalProperties: diff --git a/app/styles/editor/patches.sass b/app/styles/editor/patches.sass index 110370137..f4130ec5a 100644 --- a/app/styles/editor/patches.sass +++ b/app/styles/editor/patches.sass @@ -1,3 +1,6 @@ .patches-view .status-buttons margin-bottom: 10px + + .patch-icon + cursor: pointer diff --git a/app/views/editor/patch_modal.coffee b/app/views/editor/patch_modal.coffee index be8c4dde7..4603cbc48 100644 --- a/app/views/editor/patch_modal.coffee +++ b/app/views/editor/patch_modal.coffee @@ -8,7 +8,7 @@ module.exports = class PatchModal extends ModalView template: template plain: true modalWidthPercent: 60 - + events: 'click #withdraw-button': 'withdrawPatch' 'click #reject-button': 'rejectPatch' @@ -22,7 +22,7 @@ module.exports = class PatchModal extends ModalView else @originalSource = new @targetModel.constructor({_id:targetID}) @supermodel.loadModel @originalSource, 'source_document' - + getRenderData: -> c = super() c.isPatchCreator = @patch.get('creator') is auth.me.id @@ -30,7 +30,7 @@ module.exports = class PatchModal extends ModalView c.status = @patch.get 'status' c.patch = @patch c - + afterRender: -> return unless @supermodel.finished() headModel = null @@ -38,7 +38,7 @@ module.exports = class PatchModal extends ModalView headModel = @originalSource.clone(false) headModel.set(@targetModel.attributes) headModel.loaded = true - + pendingModel = @originalSource.clone(false) pendingModel.applyDelta(@patch.get('delta')) pendingModel.loaded = true @@ -47,18 +47,18 @@ module.exports = class PatchModal extends ModalView changeEl = @$el.find('.changes-stub') @insertSubView(@deltaView, changeEl) super() - + acceptPatch: -> delta = @deltaView.getApplicableDelta() @targetModel.applyDelta(delta) @patch.setStatus('accepted') @trigger 'accepted-patch' @hide() - + rejectPatch: -> @patch.setStatus('rejected') @hide() - + withdrawPatch: -> @patch.setStatus('withdrawn') - @hide() \ No newline at end of file + @hide() diff --git a/app/views/editor/patches_view.coffee b/app/views/editor/patches_view.coffee index 396106e86..17aef667b 100644 --- a/app/views/editor/patches_view.coffee +++ b/app/views/editor/patches_view.coffee @@ -8,7 +8,7 @@ module.exports = class PatchesView extends CocoView template: template className: 'patches-view' status: 'pending' - + events: 'change .status-buttons': 'onStatusButtonsChanged' 'click .patch-icon': 'openPatchModal' @@ -16,16 +16,16 @@ module.exports = class PatchesView extends CocoView constructor: (@model, options) -> super(options) @initPatches() - + initPatches: -> @startedLoading = false @patches = new PatchesCollection([], {}, @model, @status) - + load: -> @initPatches() @patches = @supermodel.loadCollection(@patches, 'patches').model @listenTo @patches, 'sync', @onPatchesLoaded - + onPatchesLoaded: -> ids = (p.get('creator') for p in @patches.models) jqxhrOptions = nameLoader.loadNames ids @@ -37,19 +37,20 @@ module.exports = class PatchesView extends CocoView c.patches = @patches.models c.status c - + afterRender: -> @$el.find(".#{@status}").addClass 'active' onStatusButtonsChanged: (e) -> @status = $(e.target).val() @reloadPatches() - + reloadPatches: -> @load() @render() openPatchModal: (e) -> + console.log "open patch modal" patch = _.find @patches.models, {id:$(e.target).data('patch-id')} modal = new PatchModal(patch, @model) @openModalView(modal) diff --git a/app/views/editor/thang/edit.coffee b/app/views/editor/thang/edit.coffee index 4d1cc71e0..e728e438c 100644 --- a/app/views/editor/thang/edit.coffee +++ b/app/views/editor/thang/edit.coffee @@ -197,6 +197,7 @@ module.exports = class ThangTypeEditView extends View # animation select refreshAnimation: -> + return @showRasterImage() if @thangType.get('raster') options = @getSpriteOptions() @thangType.resetSpriteSheetCache() spriteSheet = @thangType.buildSpriteSheet(options) @@ -207,6 +208,13 @@ module.exports = class ThangTypeEditView extends View @showAnimation() @updatePortrait() + showRasterImage: -> + sprite = new CocoSprite(@thangType, @getSpriteOptions()) + @currentSprite?.destroy() + @currentSprite = sprite + @showDisplayObject(sprite.displayObject) + @updateScale() + showAnimation: (animationName) -> animationName = @$el.find('#animations-select').val() unless _.isString animationName return unless animationName @@ -310,8 +318,13 @@ module.exports = class ThangTypeEditView extends View res.success => url = "/editor/thang/#{newThangType.get('slug') or newThangType.id}" - newThangType.uploadGenericPortrait -> - document.location.href = url + portraitSource = null + if @thangType.get('raster') + image = @currentSprite.imageObject.image + portraitSource = imageToPortrait image + # bit of a hacky way to get that portrait + success = -> document.location.href = url + newThangType.uploadGenericPortrait success, portraitSource clearRawData: -> @thangType.resetRawData() @@ -393,3 +406,14 @@ module.exports = class ThangTypeEditView extends View destroy: -> @camera?.destroy() super() + +imageToPortrait = (img) -> + canvas = document.createElement("canvas") + canvas.width = 100 + canvas.height = 100 + ctx = canvas.getContext("2d") + scaleX = 100 / img.width + scaleY = 100 / img.height + ctx.scale scaleX, scaleY + ctx.drawImage img, 0, 0 + canvas.toDataURL("image/png") \ No newline at end of file diff --git a/app/views/play/level/hud_view.coffee b/app/views/play/level/hud_view.coffee index 16576b334..87625d631 100644 --- a/app/views/play/level/hud_view.coffee +++ b/app/views/play/level/hud_view.coffee @@ -109,19 +109,29 @@ module.exports = class HUDView extends View @update() createAvatar: (thangType, thang, colorConfig) -> + unless thangType.isFullyLoaded() + args = arguments + unless @listeningToCreateAvatar + @listenToOnce thangType, 'sync', -> @createAvatar(args...) + @listeningToCreateAvatar = true + return + @listeningToCreateAvatar = false options = thang.getSpriteOptions() or {} options.async = false options.colorConfig = colorConfig if colorConfig - stage = thangType.getPortraitStage options wrapper = @$el.find '.thang-canvas-wrapper' - newCanvas = $(stage.canvas).addClass('thang-canvas') - wrapper.empty().append(newCanvas) team = @thang?.team or @speakerSprite?.thang?.team wrapper.removeClass (i, css) -> (css.match(/\bteam-\S+/g) or []).join ' ' wrapper.addClass "team-#{team}" - stage.update() - @stage?.stopTalking() - @stage = stage + if thangType.get('raster') + wrapper.empty().append($('<img />').attr('src', '/file/'+thangType.get('raster'))) + else + stage = thangType.getPortraitStage options + newCanvas = $(stage.canvas).addClass('thang-canvas') + wrapper.empty().append(newCanvas) + stage.update() + @stage?.stopTalking() + @stage = stage onThangBeganTalking: (e) -> return unless @stage and @thang is e.thang diff --git a/app/views/play/level/thang_avatar_view.coffee b/app/views/play/level/thang_avatar_view.coffee index 3c22fc5cb..a1af2102b 100644 --- a/app/views/play/level/thang_avatar_view.coffee +++ b/app/views/play/level/thang_avatar_view.coffee @@ -14,16 +14,28 @@ module.exports = class ThangAvatarView extends View super options @thang = options.thang @includeName = options.includeName + @thangType = @getSpriteThangType() + if not @thangType + console.error 'Thang avatar view expected a thang type to be provided.' + return + + unless @thangType.isFullyLoaded() or @thangType.loading + @thangType.fetch() + + @supermodel.loadModel @thangType, 'thang' + + getSpriteThangType: -> + thangs = @supermodel.getModels(ThangType) + thangs = (t for t in thangs when t.get('name') is @thang.spriteName) + loadedThangs = (t for t in thangs when t.isFullyLoaded()) + return loadedThangs[0] or thangs[0] # try to return one with all the goods, otherwise a projection getRenderData: (context={}) -> context = super context context.thang = @thang - thangs = @supermodel.getModels(ThangType) - thangs = (t for t in thangs when t.get('name') is @thang.spriteName) - thang = thangs[0] options = @thang?.getSpriteOptions() or {} options.async = false - context.avatarURL = thang.getPortraitSource(options) + context.avatarURL = @thangType.getPortraitSource(options) unless @thangType.loading context.includeName = @includeName context diff --git a/app/views/play/level/tome/spell_view.coffee b/app/views/play/level/tome/spell_view.coffee index e8b1e1078..4f8581d15 100644 --- a/app/views/play/level/tome/spell_view.coffee +++ b/app/views/play/level/tome/spell_view.coffee @@ -67,7 +67,7 @@ module.exports = class SpellView extends View @createFirepad() else # needs to happen after the code generating this view is complete - setTimeout @onAllLoaded, 1 + _.defer @onAllLoaded createACE: -> # Test themes and settings here: http://ace.ajax.org/build/kitchen-sink.html diff --git a/app/views/play/level_view.coffee b/app/views/play/level_view.coffee index 72c88f9c6..af874a04b 100644 --- a/app/views/play/level_view.coffee +++ b/app/views/play/level_view.coffee @@ -60,7 +60,6 @@ module.exports = class PlayLevelView extends View 'surface:world-set-up': 'onSurfaceSetUpNewWorld' 'level:session-will-save': 'onSessionWillSave' 'level:set-team': 'setTeam' - 'god:new-world-created': 'loadSoundsForWorld' 'level:started': 'onLevelStarted' 'level:loading-view-unveiled': 'onLoadingViewUnveiled' @@ -83,7 +82,6 @@ module.exports = class PlayLevelView extends View @sessionID = @getQueryVariable 'session' $(window).on('resize', @onWindowResize) - @listenToOnce(@supermodel, 'error', @onLevelLoadError) @saveScreenshot = _.throttle @saveScreenshot, 30000 if @isEditorPreview @@ -102,7 +100,6 @@ module.exports = class PlayLevelView extends View @supermodel.models = givenSupermodel.models @supermodel.collections = givenSupermodel.collections @supermodel.shouldSaveBackups = givenSupermodel.shouldSaveBackups - @god?.level = @level.serialize @supermodel if @world serializedLevel = @level.serialize(@supermodel) @@ -133,6 +130,9 @@ module.exports = class PlayLevelView extends View updateProgress: (progress) -> super(progress) + if not @worldInitialized and @levelLoader.session.loaded and @levelLoader.level.loaded and @levelLoader.world and (not @levelLoader.opponentSession or @levelLoader.opponentSession.loaded) + @grabLevelLoaderData() + @onWorldInitialized() return if @seenDocs return unless @levelLoader.session.loaded and @levelLoader.level.loaded return unless showFrequency = @levelLoader.level.get('showsGuide') @@ -144,6 +144,21 @@ module.exports = class PlayLevelView extends View return unless article.loaded @showGuide() + onWorldInitialized: -> + @worldInitialized = true + team = @getQueryVariable("team") ? @world.teamForPlayer(0) + @loadOpponentTeam(team) + @god.setLevel @level.serialize @supermodel + @god.setWorldClassMap @world.classMap + @setTeam team + @initGoalManager() + @insertSubviews ladderGame: (@level.get('type') is "ladder") + @initVolume() + @listenTo(@session, 'change:multiplayer', @onMultiplayerChanged) + @originalSessionState = $.extend(true, {}, @session.get('state')) + @register() + @controlBar.setBus(@bus) + showGuide: -> @seenDocs = true DocsModal = require './level/modal/docs_modal' @@ -165,33 +180,16 @@ module.exports = class PlayLevelView extends View if not (@levelLoader.level.get('type') in ['ladder', 'ladder-tutorial']) me.set('lastLevel', @levelID) me.save() - @grabLevelLoaderData() - team = @getQueryVariable("team") ? @world.teamForPlayer(0) - @loadOpponentTeam(team) - @god.setLevel @level.serialize @supermodel - @god.setWorldClassMap @world.classMap - @setTeam team + @levelLoader.destroy() + @levelLoader = null @initSurface() - @initGoalManager() @initScriptManager() - @insertSubviews() - @initVolume() - @listenTo(@session, 'change:multiplayer', @onMultiplayerChanged) - @originalSessionState = $.extend(true, {}, @session.get('state')) - @register() - @controlBar.setBus(@bus) - @surface.showLevel() - if @otherSession - # TODO: colorize name and cloud by team, colorize wizard by user's color config - @surface.createOpponentWizard id: @otherSession.get('creator'), name: @otherSession.get('creatorName'), team: @otherSession.get('team') grabLevelLoaderData: -> @session = @levelLoader.session @world = @levelLoader.world @level = @levelLoader.level @otherSession = @levelLoader.opponentSession - @levelLoader.destroy() - @levelLoader = null loadOpponentTeam: (myTeam) -> opponentSpells = [] @@ -212,6 +210,10 @@ module.exports = class PlayLevelView extends View @session.set 'multiplayer', false onLevelStarted: (e) -> + @surface.showLevel() + if @otherSession + # TODO: colorize name and cloud by team, colorize wizard by user's color config + @surface.createOpponentWizard id: @otherSession.get('creator'), name: @otherSession.get('creatorName'), team: @otherSession.get('team') @loadingView?.unveil() onLoadingViewUnveiled: (e) -> @@ -306,9 +308,6 @@ module.exports = class PlayLevelView extends View $('#level-done-button', @$el).hide() application.tracker?.trackEvent 'Confirmed Restart', level: @world.name, label: @world.name - onNewWorld: (e) -> - @world = e.world - onInfiniteLoop: (e) -> return unless e.firstWorld @openModalView new InfiniteLoopModal() @@ -481,11 +480,11 @@ module.exports = class PlayLevelView extends View # Dynamic sound loading - loadSoundsForWorld: (e) -> + onNewWorld: (e) -> return if @headless - world = e.world + @world = e.world thangTypes = @supermodel.getModels(ThangType) - for [spriteName, message] in world.thangDialogueSounds() + for [spriteName, message] in @world.thangDialogueSounds() continue unless thangType = _.find thangTypes, (m) -> m.get('name') is spriteName continue unless sound = AudioPlayer.soundForDialogue message, thangType.get('soundTriggers') AudioPlayer.preloadSoundReference sound diff --git a/server/commons/Handler.coffee b/server/commons/Handler.coffee index b4590b2f0..e3b7911d8 100644 --- a/server/commons/Handler.coffee +++ b/server/commons/Handler.coffee @@ -112,7 +112,7 @@ module.exports = class Handler ids = ids.split(',') if _.isString ids ids = _.uniq ids - project = {name:1} + project = {name:1, original:1} sort = {'version.major':-1, 'version.minor':-1} makeFunc = (id) => @@ -120,8 +120,8 @@ module.exports = class Handler criteria = {original:mongoose.Types.ObjectId(id)} @modelClass.findOne(criteria, project).sort(sort).exec (err, document) -> return done(err) if err - callback(null, document?.toObject() or {}) - + callback(null, document?.toObject() or null) + funcs = {} for id in ids return errors.badInput(res, "Given an invalid id: #{id}") unless Handler.isID(id) @@ -129,7 +129,7 @@ module.exports = class Handler async.parallel funcs, (err, results) -> return errors.serverError err if err - res.send results + res.send (d for d in _.values(results) when d) res.end() getPatchesFor: (req, res, id) -> diff --git a/server/levels/thangs/thang_type_handler.coffee b/server/levels/thangs/thang_type_handler.coffee index abdecd529..a8d5c05e7 100644 --- a/server/levels/thangs/thang_type_handler.coffee +++ b/server/levels/thangs/thang_type_handler.coffee @@ -5,21 +5,22 @@ ThangTypeHandler = class ThangTypeHandler extends Handler modelClass: ThangType jsonSchema: require '../../../app/schemas/models/thang_type' editableProperties: [ - 'name', - 'raw', - 'actions', - 'soundTriggers', - 'rotationType', - 'matchWorldDimensions', - 'shadow', - 'layerPriority', - 'staticImage', - 'scale', - 'positions', - 'snap', - 'components', - 'colorGroups', + 'name' + 'raw' + 'actions' + 'soundTriggers' + 'rotationType' + 'matchWorldDimensions' + 'shadow' + 'layerPriority' + 'staticImage' + 'scale' + 'positions' + 'snap' + 'components' + 'colorGroups' 'kind' + 'raster' ] hasAccess: (req) ->