Merge branch 'feature/thangload'

This commit is contained in:
Scott Erickson 2014-05-14 11:48:19 -07:00
commit 7a381df348
25 changed files with 292 additions and 111 deletions

View file

@ -2,7 +2,7 @@ var window = self;
var Global = self; var Global = self;
importScripts("/javascripts/lodash.js", "/javascripts/aether.js"); importScripts("/javascripts/lodash.js", "/javascripts/aether.js");
console.log("imported scripts!"); console.log("Aether Tome worker has finished importing scripts.");
var aethers = {}; var aethers = {};
var createAether = function (spellKey, options) var createAether = function (spellKey, options)
@ -96,4 +96,4 @@ self.addEventListener('message', function(e) {
var returnObject = {"message":message, "function":"none"}; var returnObject = {"message":message, "function":"none"};
self.postMessage(JSON.stringify(returnObject)); self.postMessage(JSON.stringify(returnObject));
} }
}, false); }, false);

View file

@ -8,4 +8,9 @@ module.exports = class CocoCollection extends Backbone.Collection
model.loaded = true for model in @models model.loaded = true for model in @models
getURL: -> getURL: ->
return if _.isString @url then @url else @url() return if _.isString @url then @url else @url()
fetch: ->
@jqxhr = super(arguments...)
@loading = true
@jqxhr

View file

@ -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)

View file

@ -4,6 +4,7 @@ LevelSystem = require 'models/LevelSystem'
Article = require 'models/Article' Article = require 'models/Article'
LevelSession = require 'models/LevelSession' LevelSession = require 'models/LevelSession'
ThangType = require 'models/ThangType' ThangType = require 'models/ThangType'
ThangNamesCollection = require 'collections/ThangNamesCollection'
CocoClass = require 'lib/CocoClass' CocoClass = require 'lib/CocoClass'
AudioPlayer = require 'lib/AudioPlayer' AudioPlayer = require 'lib/AudioPlayer'
@ -21,8 +22,10 @@ World = require 'lib/world/world'
module.exports = class LevelLoader extends CocoClass module.exports = class LevelLoader extends CocoClass
constructor: (options) -> constructor: (options) ->
@t0 = new Date().getTime()
super() super()
@supermodel = options.supermodel @supermodel = options.supermodel
@supermodel.setMaxProgress 0.2
@levelID = options.levelID @levelID = options.levelID
@sessionID = options.sessionID @sessionID = options.sessionID
@opponentSessionID = options.opponentSessionID @opponentSessionID = options.opponentSessionID
@ -103,17 +106,18 @@ module.exports = class LevelLoader extends CocoClass
objUniq = (array) -> _.uniq array, false, (arg) -> JSON.stringify(arg) objUniq = (array) -> _.uniq array, false, (arg) -> JSON.stringify(arg)
for thangID in _.uniq thangIDs worldNecessities = []
url = "/db/thang.type/#{thangID}/version"
url += "?project=true" if @headless and not @editorMode @thangIDs = _.uniq thangIDs
res = @maybeLoadURL url, ThangType, 'thang' @thangNames = new ThangNamesCollection(@thangIDs)
@listenToOnce res.model, 'sync', @buildSpriteSheetsForThangType if res worldNecessities.push @supermodel.loadCollection(@thangNames, 'thang_names')
for obj in objUniq componentVersions for obj in objUniq componentVersions
url = "/db/level.component/#{obj.original}/version/#{obj.majorVersion}" url = "/db/level.component/#{obj.original}/version/#{obj.majorVersion}"
@maybeLoadURL url, LevelComponent, 'component' worldNecessities.push @maybeLoadURL(url, LevelComponent, 'component')
for obj in objUniq systemVersions for obj in objUniq systemVersions
url = "/db/level.system/#{obj.original}/version/#{obj.majorVersion}" url = "/db/level.system/#{obj.original}/version/#{obj.majorVersion}"
@maybeLoadURL url, LevelSystem, 'system' worldNecessities.push @maybeLoadURL(url, LevelSystem, 'system')
for obj in objUniq articleVersions for obj in objUniq articleVersions
url = "/db/article/#{obj.original}/version/#{obj.majorVersion}" url = "/db/article/#{obj.original}/version/#{obj.majorVersion}"
@maybeLoadURL url, Article, 'article' @maybeLoadURL url, Article, 'article'
@ -125,16 +129,51 @@ module.exports = class LevelLoader extends CocoClass
wizard = ThangType.loadUniversalWizard() wizard = ThangType.loadUniversalWizard()
@supermodel.loadModel wizard, 'thang' @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) -> maybeLoadURL: (url, Model, resourceName) ->
return if @supermodel.getModel(url) return if @supermodel.getModel(url)
model = new Model().setURL url model = new Model().setURL url
@supermodel.loadModel(model, resourceName) @supermodel.loadModel(model, resourceName)
onSupermodelLoaded: -> onSupermodelLoaded: ->
console.log 'SuperModel for Level loaded in', new Date().getTime() - @t0, 'ms'
@loadLevelSounds() @loadLevelSounds()
@denormalizeSession() @denormalizeSession()
app.tracker.updatePlayState(@level, @session) unless @headless 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: -> denormalizeSession: ->
return if @headless or @sessionDenormalized or @spectateMode return if @headless or @sessionDenormalized or @spectateMode
@ -156,6 +195,10 @@ module.exports = class LevelLoader extends CocoClass
buildSpriteSheetsForThangType: (thangType) -> buildSpriteSheetsForThangType: (thangType) ->
return if @headless 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 @grabThangTypeTeams() unless @thangTypeTeams
for team in @thangTypeTeams[thangType.get('original')] ? [null] for team in @thangTypeTeams[thangType.get('original')] ? [null]
spriteOptions = {resolutionFactor: 4, async: false} spriteOptions = {resolutionFactor: 4, async: false}
@ -198,6 +241,7 @@ module.exports = class LevelLoader extends CocoClass
@world = new World() @world = new World()
serializedLevel = @level.serialize(@supermodel) serializedLevel = @level.serialize(@supermodel)
@world.loadFromLevel serializedLevel, false @world.loadFromLevel serializedLevel, false
console.log "World has been initialized from level loader."
# Initial Sound Loading # 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 # everything else sound wise is loaded as needed as worlds are generated
progress: -> @supermodel.progress progress: -> @supermodel.progress
destroy: ->
clearInterval @buildLoopInterval if @buildLoopInterval
super()

View file

@ -74,7 +74,7 @@ module.exports = class Simulator extends CocoClass
return return
@supermodel ?= new SuperModel() @supermodel ?= new SuperModel()
@supermodel.resetProgress()
@levelLoader = new LevelLoader supermodel: @supermodel, levelID: levelID, sessionID: @task.getFirstSessionID(), headless: true @levelLoader = new LevelLoader supermodel: @supermodel, levelID: levelID, sessionID: @task.getFirstSessionID(), headless: true
if @supermodel.finished() if @supermodel.finished()
@simulateGame() @simulateGame()

View file

@ -70,7 +70,7 @@ module.exports = CocoSprite = class CocoSprite extends CocoClass
@age = 0 @age = 0
@scaleFactor = @targetScaleFactor = 1 @scaleFactor = @targetScaleFactor = 1
@displayObject = new createjs.Container() @displayObject = new createjs.Container()
if @thangType.get('actions') if @thangType.isFullyLoaded()
@setupSprite() @setupSprite()
else else
@stillLoading = true @stillLoading = true
@ -79,9 +79,29 @@ module.exports = CocoSprite = class CocoSprite extends CocoClass
setupSprite: -> setupSprite: ->
@stillLoading = false @stillLoading = false
@actions = @thangType.getActions() if @thangType.get('raster')
@buildFromSpriteSheet @buildSpriteSheet() @isRaster = true
@createMarks() @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: -> destroy: ->
mark.destroy() for name, mark of @marks mark.destroy() for name, mark of @marks
@ -126,6 +146,7 @@ module.exports = CocoSprite = class CocoSprite extends CocoClass
queueAction: (action) -> queueAction: (action) ->
# The normal way to have an action play # The normal way to have an action play
return unless @thangType.isFullyLoaded()
action = @actions[action] if _.isString(action) action = @actions[action] if _.isString(action)
action ?= @actions.idle action ?= @actions.idle
@actionQueue = [] @actionQueue = []
@ -143,6 +164,7 @@ module.exports = CocoSprite = class CocoSprite extends CocoClass
@playAction(@actionQueue.splice(0,1)[0]) if @actionQueue.length @playAction(@actionQueue.splice(0,1)[0]) if @actionQueue.length
playAction: (action) -> playAction: (action) ->
return if @isRaster
@currentAction = action @currentAction = action
return @hide() unless action.animation or action.container or action.relatedActions return @hide() unless action.animation or action.container or action.relatedActions
@show() @show()
@ -245,15 +267,17 @@ module.exports = CocoSprite = class CocoSprite extends CocoClass
@hasMoved = true @hasMoved = true
updateScale: -> updateScale: ->
return unless @imageObject
if @thangType.get('matchWorldDimensions') and @thang if @thangType.get('matchWorldDimensions') and @thang
if @thang.width isnt @lastThangWidth or @thang.height isnt @lastThangHeight if @thang.width isnt @lastThangWidth or @thang.height isnt @lastThangHeight
[@lastThangWidth, @lastThangHeight] = [@thang.width, @thang.height]
bounds = @imageObject.getBounds() 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.scaleX = @thang.width * Camera.PPM / bounds.width
@imageObject.scaleY = @thang.height * Camera.PPM * @options.camera.y2x / bounds.height @imageObject.scaleY = @thang.height * Camera.PPM * @options.camera.y2x / bounds.height
unless @thang.spriteName is 'Beam' unless @thang.spriteName is 'Beam'
@imageObject.scaleX *= @thangType.get('scale') ? 1 @imageObject.scaleX *= @thangType.get('scale') ? 1
@imageObject.scaleY *= @thangType.get('scale') ? 1 @imageObject.scaleY *= @thangType.get('scale') ? 1
[@lastThangWidth, @lastThangHeight] = [@thang.width, @thang.height]
return return
scaleX = if @getActionProp 'flipX' then -1 else 1 scaleX = if @getActionProp 'flipX' then -1 else 1
scaleY = if @getActionProp 'flipY' 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 = -angle if angle < 0
angle = 180 - angle if angle > 90 angle = 180 - angle if angle > 90
scaleX = 0.5 + 0.5 * (90 - 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 scaleFactorX = @thang.scaleFactorX ? @scaleFactor
scaleFactorY = @thang.scaleFactorY ? @scaleFactor scaleFactorY = @thang.scaleFactorY ? @scaleFactor
@imageObject.scaleX = @originalScaleX * scaleX * scaleFactorX @imageObject.scaleX = @originalScaleX * scaleX * scaleFactorX
@ -322,6 +352,7 @@ module.exports = CocoSprite = class CocoSprite extends CocoClass
################################################## ##################################################
updateAction: -> updateAction: ->
return if @isRaster
action = @determineAction() action = @determineAction()
isDifferent = action isnt @currentRootAction or action is null isDifferent = action isnt @currentRootAction or action is null
if not action and @thang?.actionActivated and not @stopLogging 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] def = x: 0, y: {registration: 0, torso: -50, mouth: -60, aboveHead: -100}[prop]
pos = @getActionProp 'positions', prop, def pos = @getActionProp 'positions', prop, def
pos = x: pos.x, y: pos.y pos = x: pos.x, y: pos.y
scale = @getActionProp 'scale', null, 1 if not @isRaster
scale *= @options.resolutionFactor if prop is 'registration' scale = @getActionProp 'scale', null, 1
pos.x *= scale scale *= @options.resolutionFactor if prop is 'registration'
pos.y *= scale pos.x *= scale
pos.y *= scale
if @thang and prop isnt 'registration' if @thang and prop isnt 'registration'
scaleFactor = @thang.scaleFactor ? 1 scaleFactor = @thang.scaleFactor ? 1
pos.x *= @thang.scaleFactorX ? scaleFactor pos.x *= @thang.scaleFactorX ? scaleFactor
@ -658,7 +690,7 @@ module.exports = CocoSprite = class CocoSprite extends CocoClass
z = @shadow.pos.z z = @shadow.pos.z
@shadow.pos = pos @shadow.pos = pos
@shadow.pos.z = z @shadow.pos.z = z
@imageObject.gotoAndPlay(endAnimation) @imageObject.gotoAndPlay?(endAnimation)
return return
@shadow.action = 'move' @shadow.action = 'move'

View file

@ -101,4 +101,5 @@ module.exports = class Layer extends createjs.Container
cache: -> cache: ->
return unless @children.length return unless @children.length
bounds = @getBounds() bounds = @getBounds()
return unless bounds
super bounds.x, bounds.y, bounds.width, bounds.height, 2 super bounds.x, bounds.y, bounds.width, bounds.height, 2

View file

@ -200,7 +200,7 @@ module.exports = class Mark extends CocoClass
Backbone.Mediator.publish 'sprite:loaded' Backbone.Mediator.publish 'sprite:loaded'
update: (pos=null) -> update: (pos=null) ->
return false unless @on and @mark return false unless @on and @mark and @sprite?.thangType.isFullyLoaded()
@mark.visible = not @hidden @mark.visible = not @hidden
@updatePosition pos @updatePosition pos
@updateRotation() @updateRotation()
@ -242,7 +242,7 @@ module.exports = class Mark extends CocoClass
oldMark.parent.removeChild oldMark oldMark.parent.removeChild oldMark
return unless @name in ["selection", "target", "repair", "highlight"] return unless @name in ["selection", "target", "repair", "highlight"]
scale = 0.5 scale = 0.5
if @sprite if @sprite?.imageObject
size = @sprite.getAverageDimension() size = @sprite.getAverageDimension()
size += 60 if @name is 'selection' size += 60 if @name is 'selection'
size += 60 if @name is 'repair' size += 60 if @name is 'repair'

View file

@ -47,7 +47,7 @@ module.exports = class SpriteBoss extends CocoClass
toString: -> "<SpriteBoss: #{@spriteArray.length} sprites>" toString: -> "<SpriteBoss: #{@spriteArray.length} sprites>"
thangTypeFor: (type) -> 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: -> createLayers: ->
@spriteLayers = {} @spriteLayers = {}
@ -144,7 +144,11 @@ module.exports = class SpriteBoss extends CocoClass
addThangToSprites: (thang, layer=null) -> addThangToSprites: (thang, layer=null) ->
return console.warn 'Tried to add Thang to the surface it already has:', thang.id if @sprites[thang.id] 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 = @createSpriteOptions thang: thang
options.resolutionFactor = if thangType.get('kind') is 'Floor' then 2 else 4 options.resolutionFactor = if thangType.get('kind') is 'Floor' then 2 else 4
sprite = new CocoSprite thangType, options sprite = new CocoSprite thangType, options
@ -196,6 +200,8 @@ module.exports = class SpriteBoss extends CocoClass
cache: (update=false) -> cache: (update=false) ->
return if @cached and not update return if @cached and not update
wallSprites = (sprite for sprite in @spriteArray when sprite.thangType?.get('name').search(/(dungeon|indoor).wall/i) isnt -1) 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) walls = (sprite.thang for sprite in wallSprites)
@world.calculateBounds() @world.calculateBounds()
wallGrid = new Grid walls, @world.size()... wallGrid = new Grid walls, @world.size()...

View file

@ -84,6 +84,8 @@ module.exports = Surface = class Surface extends CocoClass
@initAudio() @initAudio()
@onResize = _.debounce @onResize, 500 @onResize = _.debounce @onResize, 500
$(window).on 'resize', @onResize $(window).on 'resize', @onResize
if @world.ended
_.defer => @setWorld @world
destroy: -> destroy: ->
@dead = true @dead = true

View file

@ -699,6 +699,7 @@
user_schema: "User Schema" user_schema: "User Schema"
user_profile: "User Profile" user_profile: "User Profile"
patches: "Patches" patches: "Patches"
patched_model: "Source Document"
model: "Model" model: "Model"
system: "System" system: "System"
component: "Component" component: "Component"
@ -709,10 +710,12 @@
opponent_session: "Opponent Session" opponent_session: "Opponent Session"
article: "Article" article: "Article"
user_names: "User Names" user_names: "User Names"
thang_names: "Thang Names"
files: "Files" files: "Files"
top_simulators: "Top Simulators" top_simulators: "Top Simulators"
source_document: "Source Document" source_document: "Source Document"
document: "Document" # note to diplomats: not a physical document, a document in MongoDB, ie a record in a database document: "Document" # note to diplomats: not a physical document, a document in MongoDB, ie a record in a database
sprite_sheet: "Sprite Sheet"
delta: delta:
added: "Added" added: "Added"

View file

@ -37,6 +37,8 @@ class CocoModel extends Backbone.Model
@loading = false @loading = false
@markToRevert() @markToRevert()
@loadFromBackup() @loadFromBackup()
getNormalizedURL: -> "#{@urlRoot}/#{@id}"
set: -> set: ->
res = super(arguments...) res = super(arguments...)
@ -76,9 +78,9 @@ class CocoModel extends Backbone.Model
return super attrs, options return super attrs, options
fetch: -> fetch: ->
res = super(arguments...) @jqxhr = super(arguments...)
@loading = true @loading = true
res @jqxhr
markToRevert: -> markToRevert: ->
if @type() is 'ThangType' if @type() is 'ThangType'

View file

@ -5,6 +5,7 @@ module.exports = class SuperModel extends Backbone.Model
@progress = 0 @progress = 0
@resources = {} @resources = {}
@rid = 0 @rid = 0
@maxProgress = 1
@models = {} @models = {}
@collections = {} @collections = {}
@ -19,7 +20,6 @@ module.exports = class SuperModel extends Backbone.Model
loadModel: (model, name, fetchOptions, value=1) -> loadModel: (model, name, fetchOptions, value=1) ->
cachedModel = @getModelByURL(model.getURL()) cachedModel = @getModelByURL(model.getURL())
if cachedModel if cachedModel
console.debug 'Model cache hit', cachedModel.getURL(), 'already loaded', cachedModel.loaded
if cachedModel.loaded if cachedModel.loaded
res = @addModelResource(cachedModel, name, fetchOptions, 0) res = @addModelResource(cachedModel, name, fetchOptions, 0)
res.markLoaded() res.markLoaded()
@ -96,7 +96,7 @@ module.exports = class SuperModel extends Backbone.Model
@registerCollection(collection) @registerCollection(collection)
registerCollection: (collection) -> registerCollection: (collection) ->
@collections[collection.getURL()] = collection @collections[collection.getURL()] = collection if collection.isCachable
# consolidate models # consolidate models
for model, i in collection.models for model, i in collection.models
cachedModel = @getModelByURL(model.getURL()) cachedModel = @getModelByURL(model.getURL())
@ -141,7 +141,7 @@ module.exports = class SuperModel extends Backbone.Model
@listenToOnce(resource, 'loaded', @onResourceLoaded) @listenToOnce(resource, 'loaded', @onResourceLoaded)
@listenTo(resource, 'failed', @onResourceFailed) @listenTo(resource, 'failed', @onResourceFailed)
@denom += value @denom += value
@updateProgress() if @denom _.defer @updateProgress if @denom
onResourceLoaded: (r) -> onResourceLoaded: (r) ->
@num += r.value @num += r.value
@ -155,11 +155,18 @@ module.exports = class SuperModel extends Backbone.Model
# a bunch of things load all at once. # a bunch of things load all at once.
# So make sure we only emit events if @progress has changed. # So make sure we only emit events if @progress has changed.
newProg = if @denom then @num / @denom else 1 newProg = if @denom then @num / @denom else 1
return if @progress is newProg newProg = Math.min @maxProgress, newProg
return if @progress >= newProg
@progress = newProg @progress = newProg
@trigger('update-progress', @progress) @trigger('update-progress', @progress)
@trigger('loaded-all') if @finished() @trigger('loaded-all') if @finished()
setMaxProgress: (@maxProgress) ->
resetProgress: -> @progress = 0
clearMaxProgress: ->
@maxProgress = 1
_.defer @updateProgress
getProgress: -> return @progress getProgress: -> return @progress
getResource: (rid) -> getResource: (rid) ->
@ -202,6 +209,7 @@ class ModelResource extends Resource
super(name, value) super(name, value)
@model = modelOrCollection @model = modelOrCollection
@fetchOptions = fetchOptions @fetchOptions = fetchOptions
@jqxhr = @model.jqxhr
load: -> load: ->
@markLoading() @markLoading()

View file

@ -26,12 +26,18 @@ module.exports = class ThangType extends CocoModel
@buildActions() @buildActions()
@spriteSheets = {} @spriteSheets = {}
@building = {} @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: -> getActions: ->
return {} unless @isFullyLoaded()
return @actions or @buildActions() return @actions or @buildActions()
buildActions: -> buildActions: ->
@actions = $.extend(true, {}, @get('actions') or {}) return null unless @isFullyLoaded()
@actions = $.extend(true, {}, @get('actions'))
for name, action of @actions for name, action of @actions
action.name = name action.name = name
for relatedName, relatedAction of action.relatedActions ? {} for relatedName, relatedAction of action.relatedActions ? {}
@ -52,9 +58,12 @@ module.exports = class ThangType extends CocoModel
options options
buildSpriteSheet: (options) -> buildSpriteSheet: (options) ->
return false unless @isFullyLoaded()
@options = @fillOptions options @options = @fillOptions options
key = @spriteSheetKey(@options) key = @spriteSheetKey(@options)
if ss = @spriteSheets[key] then return ss
return if @building[key] return if @building[key]
@t0 = new Date().getTime()
@initBuild(options) @initBuild(options)
@addGeneralFrames() unless @options.portraitOnly @addGeneralFrames() unless @options.portraitOnly
@addPortrait() @addPortrait()
@ -144,9 +153,8 @@ module.exports = class ThangType extends CocoModel
@builder.buildAsync() unless buildQueue.length > 1 @builder.buildAsync() unless buildQueue.length > 1
@builder.on 'complete', @onBuildSpriteSheetComplete, @, true, key @builder.on 'complete', @onBuildSpriteSheetComplete, @, true, key
return true return true
t0 = new Date()
spriteSheet = @builder.build() 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 @spriteSheets[key] = spriteSheet
delete @building[key] delete @building[key]
spriteSheet spriteSheet
@ -180,6 +188,7 @@ module.exports = class ThangType extends CocoModel
stage?.toDataURL() stage?.toDataURL()
getPortraitStage: (spriteOptionsOrKey, size=100) -> getPortraitStage: (spriteOptionsOrKey, size=100) ->
return unless @isFullyLoaded()
key = spriteOptionsOrKey key = spriteOptionsOrKey
key = if _.isString(key) then key else @spriteSheetKey(@fillOptions(key)) key = if _.isString(key) then key else @spriteSheetKey(@fillOptions(key))
spriteSheet = @spriteSheets[key] spriteSheet = @spriteSheets[key]
@ -210,8 +219,8 @@ module.exports = class ThangType extends CocoModel
@tick = null @tick = null
stage stage
uploadGenericPortrait: (callback) -> uploadGenericPortrait: (callback, src) ->
src = @getPortraitSource() src ?= @getPortraitSource()
return callback?() unless src return callback?() unless src
src = src.replace('data:image/png;base64,', '').replace(/\ /g, '+') src = src.replace('data:image/png;base64,', '').replace(/\ /g, '+')
body = body =

View file

@ -123,6 +123,7 @@ _.extend ThangTypeSchema.properties,
title: 'Scale' title: 'Scale'
type: 'number' type: 'number'
positions: PositionsSchema positions: PositionsSchema
raster: { type: 'string', format: 'image-file', title: 'Raster Image' }
colorGroups: c.object colorGroups: c.object
title: 'Color Groups' title: 'Color Groups'
additionalProperties: additionalProperties:

View file

@ -1,3 +1,6 @@
.patches-view .patches-view
.status-buttons .status-buttons
margin-bottom: 10px margin-bottom: 10px
.patch-icon
cursor: pointer

View file

@ -8,7 +8,7 @@ module.exports = class PatchModal extends ModalView
template: template template: template
plain: true plain: true
modalWidthPercent: 60 modalWidthPercent: 60
events: events:
'click #withdraw-button': 'withdrawPatch' 'click #withdraw-button': 'withdrawPatch'
'click #reject-button': 'rejectPatch' 'click #reject-button': 'rejectPatch'
@ -22,7 +22,7 @@ module.exports = class PatchModal extends ModalView
else else
@originalSource = new @targetModel.constructor({_id:targetID}) @originalSource = new @targetModel.constructor({_id:targetID})
@supermodel.loadModel @originalSource, 'source_document' @supermodel.loadModel @originalSource, 'source_document'
getRenderData: -> getRenderData: ->
c = super() c = super()
c.isPatchCreator = @patch.get('creator') is auth.me.id 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.status = @patch.get 'status'
c.patch = @patch c.patch = @patch
c c
afterRender: -> afterRender: ->
return unless @supermodel.finished() return unless @supermodel.finished()
headModel = null headModel = null
@ -38,7 +38,7 @@ module.exports = class PatchModal extends ModalView
headModel = @originalSource.clone(false) headModel = @originalSource.clone(false)
headModel.set(@targetModel.attributes) headModel.set(@targetModel.attributes)
headModel.loaded = true headModel.loaded = true
pendingModel = @originalSource.clone(false) pendingModel = @originalSource.clone(false)
pendingModel.applyDelta(@patch.get('delta')) pendingModel.applyDelta(@patch.get('delta'))
pendingModel.loaded = true pendingModel.loaded = true
@ -47,18 +47,18 @@ module.exports = class PatchModal extends ModalView
changeEl = @$el.find('.changes-stub') changeEl = @$el.find('.changes-stub')
@insertSubView(@deltaView, changeEl) @insertSubView(@deltaView, changeEl)
super() super()
acceptPatch: -> acceptPatch: ->
delta = @deltaView.getApplicableDelta() delta = @deltaView.getApplicableDelta()
@targetModel.applyDelta(delta) @targetModel.applyDelta(delta)
@patch.setStatus('accepted') @patch.setStatus('accepted')
@trigger 'accepted-patch' @trigger 'accepted-patch'
@hide() @hide()
rejectPatch: -> rejectPatch: ->
@patch.setStatus('rejected') @patch.setStatus('rejected')
@hide() @hide()
withdrawPatch: -> withdrawPatch: ->
@patch.setStatus('withdrawn') @patch.setStatus('withdrawn')
@hide() @hide()

View file

@ -8,7 +8,7 @@ module.exports = class PatchesView extends CocoView
template: template template: template
className: 'patches-view' className: 'patches-view'
status: 'pending' status: 'pending'
events: events:
'change .status-buttons': 'onStatusButtonsChanged' 'change .status-buttons': 'onStatusButtonsChanged'
'click .patch-icon': 'openPatchModal' 'click .patch-icon': 'openPatchModal'
@ -16,16 +16,16 @@ module.exports = class PatchesView extends CocoView
constructor: (@model, options) -> constructor: (@model, options) ->
super(options) super(options)
@initPatches() @initPatches()
initPatches: -> initPatches: ->
@startedLoading = false @startedLoading = false
@patches = new PatchesCollection([], {}, @model, @status) @patches = new PatchesCollection([], {}, @model, @status)
load: -> load: ->
@initPatches() @initPatches()
@patches = @supermodel.loadCollection(@patches, 'patches').model @patches = @supermodel.loadCollection(@patches, 'patches').model
@listenTo @patches, 'sync', @onPatchesLoaded @listenTo @patches, 'sync', @onPatchesLoaded
onPatchesLoaded: -> onPatchesLoaded: ->
ids = (p.get('creator') for p in @patches.models) ids = (p.get('creator') for p in @patches.models)
jqxhrOptions = nameLoader.loadNames ids jqxhrOptions = nameLoader.loadNames ids
@ -37,19 +37,20 @@ module.exports = class PatchesView extends CocoView
c.patches = @patches.models c.patches = @patches.models
c.status c.status
c c
afterRender: -> afterRender: ->
@$el.find(".#{@status}").addClass 'active' @$el.find(".#{@status}").addClass 'active'
onStatusButtonsChanged: (e) -> onStatusButtonsChanged: (e) ->
@status = $(e.target).val() @status = $(e.target).val()
@reloadPatches() @reloadPatches()
reloadPatches: -> reloadPatches: ->
@load() @load()
@render() @render()
openPatchModal: (e) -> openPatchModal: (e) ->
console.log "open patch modal"
patch = _.find @patches.models, {id:$(e.target).data('patch-id')} patch = _.find @patches.models, {id:$(e.target).data('patch-id')}
modal = new PatchModal(patch, @model) modal = new PatchModal(patch, @model)
@openModalView(modal) @openModalView(modal)

View file

@ -197,6 +197,7 @@ module.exports = class ThangTypeEditView extends View
# animation select # animation select
refreshAnimation: -> refreshAnimation: ->
return @showRasterImage() if @thangType.get('raster')
options = @getSpriteOptions() options = @getSpriteOptions()
@thangType.resetSpriteSheetCache() @thangType.resetSpriteSheetCache()
spriteSheet = @thangType.buildSpriteSheet(options) spriteSheet = @thangType.buildSpriteSheet(options)
@ -207,6 +208,13 @@ module.exports = class ThangTypeEditView extends View
@showAnimation() @showAnimation()
@updatePortrait() @updatePortrait()
showRasterImage: ->
sprite = new CocoSprite(@thangType, @getSpriteOptions())
@currentSprite?.destroy()
@currentSprite = sprite
@showDisplayObject(sprite.displayObject)
@updateScale()
showAnimation: (animationName) -> showAnimation: (animationName) ->
animationName = @$el.find('#animations-select').val() unless _.isString animationName animationName = @$el.find('#animations-select').val() unless _.isString animationName
return unless animationName return unless animationName
@ -310,8 +318,13 @@ module.exports = class ThangTypeEditView extends View
res.success => res.success =>
url = "/editor/thang/#{newThangType.get('slug') or newThangType.id}" url = "/editor/thang/#{newThangType.get('slug') or newThangType.id}"
newThangType.uploadGenericPortrait -> portraitSource = null
document.location.href = url 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: -> clearRawData: ->
@thangType.resetRawData() @thangType.resetRawData()
@ -393,3 +406,14 @@ module.exports = class ThangTypeEditView extends View
destroy: -> destroy: ->
@camera?.destroy() @camera?.destroy()
super() 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")

View file

@ -109,19 +109,29 @@ module.exports = class HUDView extends View
@update() @update()
createAvatar: (thangType, thang, colorConfig) -> 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 = thang.getSpriteOptions() or {}
options.async = false options.async = false
options.colorConfig = colorConfig if colorConfig options.colorConfig = colorConfig if colorConfig
stage = thangType.getPortraitStage options
wrapper = @$el.find '.thang-canvas-wrapper' wrapper = @$el.find '.thang-canvas-wrapper'
newCanvas = $(stage.canvas).addClass('thang-canvas')
wrapper.empty().append(newCanvas)
team = @thang?.team or @speakerSprite?.thang?.team team = @thang?.team or @speakerSprite?.thang?.team
wrapper.removeClass (i, css) -> (css.match(/\bteam-\S+/g) or []).join ' ' wrapper.removeClass (i, css) -> (css.match(/\bteam-\S+/g) or []).join ' '
wrapper.addClass "team-#{team}" wrapper.addClass "team-#{team}"
stage.update() if thangType.get('raster')
@stage?.stopTalking() wrapper.empty().append($('<img />').attr('src', '/file/'+thangType.get('raster')))
@stage = stage else
stage = thangType.getPortraitStage options
newCanvas = $(stage.canvas).addClass('thang-canvas')
wrapper.empty().append(newCanvas)
stage.update()
@stage?.stopTalking()
@stage = stage
onThangBeganTalking: (e) -> onThangBeganTalking: (e) ->
return unless @stage and @thang is e.thang return unless @stage and @thang is e.thang

View file

@ -14,16 +14,28 @@ module.exports = class ThangAvatarView extends View
super options super options
@thang = options.thang @thang = options.thang
@includeName = options.includeName @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={}) -> getRenderData: (context={}) ->
context = super context context = super context
context.thang = @thang 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 = @thang?.getSpriteOptions() or {}
options.async = false options.async = false
context.avatarURL = thang.getPortraitSource(options) context.avatarURL = @thangType.getPortraitSource(options) unless @thangType.loading
context.includeName = @includeName context.includeName = @includeName
context context

View file

@ -67,7 +67,7 @@ module.exports = class SpellView extends View
@createFirepad() @createFirepad()
else else
# needs to happen after the code generating this view is complete # needs to happen after the code generating this view is complete
setTimeout @onAllLoaded, 1 _.defer @onAllLoaded
createACE: -> createACE: ->
# Test themes and settings here: http://ace.ajax.org/build/kitchen-sink.html # Test themes and settings here: http://ace.ajax.org/build/kitchen-sink.html

View file

@ -60,7 +60,6 @@ module.exports = class PlayLevelView extends View
'surface:world-set-up': 'onSurfaceSetUpNewWorld' 'surface:world-set-up': 'onSurfaceSetUpNewWorld'
'level:session-will-save': 'onSessionWillSave' 'level:session-will-save': 'onSessionWillSave'
'level:set-team': 'setTeam' 'level:set-team': 'setTeam'
'god:new-world-created': 'loadSoundsForWorld'
'level:started': 'onLevelStarted' 'level:started': 'onLevelStarted'
'level:loading-view-unveiled': 'onLoadingViewUnveiled' 'level:loading-view-unveiled': 'onLoadingViewUnveiled'
@ -83,7 +82,6 @@ module.exports = class PlayLevelView extends View
@sessionID = @getQueryVariable 'session' @sessionID = @getQueryVariable 'session'
$(window).on('resize', @onWindowResize) $(window).on('resize', @onWindowResize)
@listenToOnce(@supermodel, 'error', @onLevelLoadError)
@saveScreenshot = _.throttle @saveScreenshot, 30000 @saveScreenshot = _.throttle @saveScreenshot, 30000
if @isEditorPreview if @isEditorPreview
@ -102,7 +100,6 @@ module.exports = class PlayLevelView extends View
@supermodel.models = givenSupermodel.models @supermodel.models = givenSupermodel.models
@supermodel.collections = givenSupermodel.collections @supermodel.collections = givenSupermodel.collections
@supermodel.shouldSaveBackups = givenSupermodel.shouldSaveBackups @supermodel.shouldSaveBackups = givenSupermodel.shouldSaveBackups
@god?.level = @level.serialize @supermodel @god?.level = @level.serialize @supermodel
if @world if @world
serializedLevel = @level.serialize(@supermodel) serializedLevel = @level.serialize(@supermodel)
@ -133,6 +130,9 @@ module.exports = class PlayLevelView extends View
updateProgress: (progress) -> updateProgress: (progress) ->
super(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 if @seenDocs
return unless @levelLoader.session.loaded and @levelLoader.level.loaded return unless @levelLoader.session.loaded and @levelLoader.level.loaded
return unless showFrequency = @levelLoader.level.get('showsGuide') return unless showFrequency = @levelLoader.level.get('showsGuide')
@ -144,6 +144,21 @@ module.exports = class PlayLevelView extends View
return unless article.loaded return unless article.loaded
@showGuide() @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: -> showGuide: ->
@seenDocs = true @seenDocs = true
DocsModal = require './level/modal/docs_modal' 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']) if not (@levelLoader.level.get('type') in ['ladder', 'ladder-tutorial'])
me.set('lastLevel', @levelID) me.set('lastLevel', @levelID)
me.save() me.save()
@grabLevelLoaderData() @levelLoader.destroy()
team = @getQueryVariable("team") ? @world.teamForPlayer(0) @levelLoader = null
@loadOpponentTeam(team)
@god.setLevel @level.serialize @supermodel
@god.setWorldClassMap @world.classMap
@setTeam team
@initSurface() @initSurface()
@initGoalManager()
@initScriptManager() @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: -> grabLevelLoaderData: ->
@session = @levelLoader.session @session = @levelLoader.session
@world = @levelLoader.world @world = @levelLoader.world
@level = @levelLoader.level @level = @levelLoader.level
@otherSession = @levelLoader.opponentSession @otherSession = @levelLoader.opponentSession
@levelLoader.destroy()
@levelLoader = null
loadOpponentTeam: (myTeam) -> loadOpponentTeam: (myTeam) ->
opponentSpells = [] opponentSpells = []
@ -212,6 +210,10 @@ module.exports = class PlayLevelView extends View
@session.set 'multiplayer', false @session.set 'multiplayer', false
onLevelStarted: (e) -> 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() @loadingView?.unveil()
onLoadingViewUnveiled: (e) -> onLoadingViewUnveiled: (e) ->
@ -306,9 +308,6 @@ module.exports = class PlayLevelView extends View
$('#level-done-button', @$el).hide() $('#level-done-button', @$el).hide()
application.tracker?.trackEvent 'Confirmed Restart', level: @world.name, label: @world.name application.tracker?.trackEvent 'Confirmed Restart', level: @world.name, label: @world.name
onNewWorld: (e) ->
@world = e.world
onInfiniteLoop: (e) -> onInfiniteLoop: (e) ->
return unless e.firstWorld return unless e.firstWorld
@openModalView new InfiniteLoopModal() @openModalView new InfiniteLoopModal()
@ -481,11 +480,11 @@ module.exports = class PlayLevelView extends View
# Dynamic sound loading # Dynamic sound loading
loadSoundsForWorld: (e) -> onNewWorld: (e) ->
return if @headless return if @headless
world = e.world @world = e.world
thangTypes = @supermodel.getModels(ThangType) 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 thangType = _.find thangTypes, (m) -> m.get('name') is spriteName
continue unless sound = AudioPlayer.soundForDialogue message, thangType.get('soundTriggers') continue unless sound = AudioPlayer.soundForDialogue message, thangType.get('soundTriggers')
AudioPlayer.preloadSoundReference sound AudioPlayer.preloadSoundReference sound

View file

@ -112,7 +112,7 @@ module.exports = class Handler
ids = ids.split(',') if _.isString ids ids = ids.split(',') if _.isString ids
ids = _.uniq ids ids = _.uniq ids
project = {name:1} project = {name:1, original:1}
sort = {'version.major':-1, 'version.minor':-1} sort = {'version.major':-1, 'version.minor':-1}
makeFunc = (id) => makeFunc = (id) =>
@ -120,8 +120,8 @@ module.exports = class Handler
criteria = {original:mongoose.Types.ObjectId(id)} criteria = {original:mongoose.Types.ObjectId(id)}
@modelClass.findOne(criteria, project).sort(sort).exec (err, document) -> @modelClass.findOne(criteria, project).sort(sort).exec (err, document) ->
return done(err) if err return done(err) if err
callback(null, document?.toObject() or {}) callback(null, document?.toObject() or null)
funcs = {} funcs = {}
for id in ids for id in ids
return errors.badInput(res, "Given an invalid id: #{id}") unless Handler.isID(id) 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) -> async.parallel funcs, (err, results) ->
return errors.serverError err if err return errors.serverError err if err
res.send results res.send (d for d in _.values(results) when d)
res.end() res.end()
getPatchesFor: (req, res, id) -> getPatchesFor: (req, res, id) ->

View file

@ -5,21 +5,22 @@ ThangTypeHandler = class ThangTypeHandler extends Handler
modelClass: ThangType modelClass: ThangType
jsonSchema: require '../../../app/schemas/models/thang_type' jsonSchema: require '../../../app/schemas/models/thang_type'
editableProperties: [ editableProperties: [
'name', 'name'
'raw', 'raw'
'actions', 'actions'
'soundTriggers', 'soundTriggers'
'rotationType', 'rotationType'
'matchWorldDimensions', 'matchWorldDimensions'
'shadow', 'shadow'
'layerPriority', 'layerPriority'
'staticImage', 'staticImage'
'scale', 'scale'
'positions', 'positions'
'snap', 'snap'
'components', 'components'
'colorGroups', 'colorGroups'
'kind' 'kind'
'raster'
] ]
hasAccess: (req) -> hasAccess: (req) ->