diff --git a/app/lib/sprites/SpriteParser.coffee b/app/lib/sprites/SpriteParser.coffee
index 1fb1598f5..75288c5fa 100644
--- a/app/lib/sprites/SpriteParser.coffee
+++ b/app/lib/sprites/SpriteParser.coffee
@@ -82,6 +82,7 @@ module.exports = class SpriteParser
     shortKey = @shapeLongKeys[longKey]
     unless shortKey?
       shortKey = '' + _.size @thangType.shapes
+      shortKey += '+' while @thangType.shapes[shortKey]
       @thangType.shapes[shortKey] = shape
       @shapeLongKeys[longKey] = shortKey
     return shortKey
diff --git a/app/lib/surface/CocoSprite.coffee b/app/lib/surface/CocoSprite.coffee
index 4de5f03f0..5f4abcf84 100644
--- a/app/lib/surface/CocoSprite.coffee
+++ b/app/lib/surface/CocoSprite.coffee
@@ -129,7 +129,7 @@ module.exports = CocoSprite = class CocoSprite extends CocoClass
     reg = @getOffset 'registration'
     @imageObject.regX = -reg.x
     @imageObject.regY = -reg.y
-    if @currentRootAction.name is 'move'
+    if @currentRootAction.name is 'move' and action.frames
       start = Math.floor(Math.random() * action.frames.length)
       @imageObject.currentAnimationFrame = start
 
@@ -375,6 +375,7 @@ module.exports = CocoSprite = class CocoSprite extends CocoClass
     e = _.clone(e)
     e.sprite = @
     e.blurb ?= '...'
+    e.thang = @thang
     Backbone.Mediator.publish 'sprite:speech-updated', e
 
   isTalking: ->
@@ -427,5 +428,5 @@ module.exports = CocoSprite = class CocoSprite extends CocoClass
     delay = if withDelay and sound.delay then 1000 * sound.delay / createjs.Ticker.getFPS() else 0
     name = AudioPlayer.nameForSoundReference sound
     instance = createjs.Sound.play name, "none", delay, 0, 0, volume
-    #console.log @thang?.id, "played sound", name, "with delay", delay, "volume", volume, "and got sound instance", instance
+#    console.log @thang?.id, "played sound", name, "with delay", delay, "volume", volume, "and got sound instance", instance
     instance
\ No newline at end of file
diff --git a/app/lib/surface/SpriteBoss.coffee b/app/lib/surface/SpriteBoss.coffee
index 2c0a8ae3f..e8c75c907 100644
--- a/app/lib/surface/SpriteBoss.coffee
+++ b/app/lib/surface/SpriteBoss.coffee
@@ -233,7 +233,7 @@ module.exports = class SpriteBoss extends CocoClass
       sprite?.selected = true
       @selectedSprite = sprite
     alive = sprite?.thang.health > 0
-    sprite.playSound 'selected' if alive and not @suppressSelectionSounds
+
     Backbone.Mediator.publish "surface:sprite-selected",
       thang: if sprite then sprite.thang else null
       sprite: sprite
@@ -241,6 +241,14 @@ module.exports = class SpriteBoss extends CocoClass
       originalEvent: e
       worldPos: worldPos
 
+    if alive and not @suppressSelectionSounds
+      instance = sprite.playSound 'selected'
+      if instance.playState is 'playSucceeded'
+        Backbone.Mediator.publish 'thang-began-talking', thang: sprite?.thang
+        instance.addEventListener 'complete', ->
+          Backbone.Mediator.publish 'thang-finished-talking', thang: sprite?.thang
+      console.log 'select...', instance.playState
+
   # Marks
 
   updateSelection: ->
diff --git a/app/lib/surface/path.coffee b/app/lib/surface/path.coffee
index db5cff2c3..ecf7a729c 100644
--- a/app/lib/surface/path.coffee
+++ b/app/lib/surface/path.coffee
@@ -179,7 +179,7 @@ module.exports.Trailmaster = class Trailmaster
         animation = sprite.getActionDirection(animation) ? animation  # no idea if this ever works
         clone.gotoAndStop animation.name
         # TODO: use action-specific framerate here?
-        clone.currentAnimationFrame = Math.min(@clock % (animation.frames.length * 3), animation.frames.length - 1)
+#        clone.currentAnimationFrame = Math.min(@clock % (animation.frames.length * 3), animation.frames.length - 1)
       sprites.push(clone)
       lastPos = x: thang.pos.x, y: thang.pos.y
       lastAction = action.name
diff --git a/app/models/ThangType.coffee b/app/models/ThangType.coffee
index 66646c280..df8ac802b 100644
--- a/app/models/ThangType.coffee
+++ b/app/models/ThangType.coffee
@@ -18,19 +18,6 @@ module.exports = class ThangType extends CocoModel
   resetRawData: ->
     @set('raw', {shapes:{}, containers:{}, animations:{}})
 
-  requiredRawAnimations: ->
-    required = []
-    for name, action of @get('actions')
-      continue if name is 'portrait'
-      allActions = [action].concat(_.values (action.relatedActions ? {}))
-      for a in allActions when a.animation
-        scale = if name is 'portrait' then a.scale or 1 else a.scale or @get('scale') or 1
-        animation = {animation: a.animation, scale: scale}
-        animation.portrait = name is 'portrait'
-        unless _.find(required, (r) -> _.isEqual r, animation)
-          required.push animation
-    required
-    
   resetSpriteSheetCache: ->
     @buildActions()
     @spriteSheets = {}
@@ -87,8 +74,7 @@ module.exports = class ThangType extends CocoModel
       mc.nominalBounds = mc.frameBounds = null # override what the movie clip says on bounding
       @builder.addMovieClip(mc, rect, scale)
       frames = @builder._animations[portrait.animation].frames
-      frames = @normalizeFrames(portrait.frames, frames[0]) if portrait.frames?
-      portrait.frames = frames
+      frames = @mapFrames(portrait.frames, frames[0]) if portrait.frames?
       @builder.addAnimation 'portrait', frames, true
     else if portrait.container
       s = @vectorParser.buildContainerFromStore(portrait.container)
@@ -108,7 +94,6 @@ module.exports = class ThangType extends CocoModel
       scale = action.scale ? @get('scale') ? 1
       frames = framesMap[scale + "_" + action.animation]
       frames = @mapFrames(action.frames, frames[0]) if action.frames?
-      action.frames = frames  # Keep generated frame numbers around
       next = true
       next = action.goesTo if action.goesTo
       next = false if action.loops is false
@@ -120,7 +105,20 @@ module.exports = class ThangType extends CocoModel
       s = @vectorParser.buildContainerFromStore(action.container)
       frame = @builder.addFrame(s, s.bounds, scale)
       @builder.addAnimation name, [frame], false
-      
+
+  requiredRawAnimations: ->
+    required = []
+    for name, action of @get('actions')
+      continue if name is 'portrait'
+      allActions = [action].concat(_.values (action.relatedActions ? {}))
+      for a in allActions when a.animation
+        scale = if name is 'portrait' then a.scale or 1 else a.scale or @get('scale') or 1
+        animation = {animation: a.animation, scale: scale}
+        animation.portrait = name is 'portrait'
+        unless _.find(required, (r) -> _.isEqual r, animation)
+          required.push animation
+    required
+
   mapFrames: (frames, frameOffset) ->
     return frames unless _.isString(frames) # don't accidentally do this again
     (parseInt(f, 10) + frameOffset for f in frames.split(','))
@@ -148,11 +146,16 @@ module.exports = class ThangType extends CocoModel
 
   getPortraitImage: (spriteOptionsOrKey, size=100) ->
     src = @getPortraitSource(spriteOptionsOrKey, size)
+    return null unless src
     $('<img />').attr('src', src)
 
   getPortraitSource: (spriteOptionsOrKey, size=100) ->
+    stage = @getPortraitStage(spriteOptionsOrKey, size)
+    stage?.toDataURL()
+
+  getPortraitStage: (spriteOptionsOrKey, size=100) ->
     key = spriteOptionsOrKey
-    key = if _.isObject(key) then @spriteSheetKey(key) else key
+    key = if _.isString(key) then key else @spriteSheetKey(@fillOptions(key))
     spriteSheet = @spriteSheets[key]
     spriteSheet ?= @buildSpriteSheet({portraitOnly:true})
     return unless spriteSheet
@@ -165,11 +168,21 @@ module.exports = class ThangType extends CocoModel
     sprite.gotoAndStop 'portrait'
     stage.addChild(sprite)
     stage.update()
-    stage.toDataURL()
+    stage.startTalking = ->
+      sprite.gotoAndPlay 'portrait'
+      return if @tick
+      @tick = => @update()
+      createjs.Ticker.addEventListener 'tick', @tick
+    stage.stopTalking = ->
+      sprite.gotoAndStop 'portrait'
+      @update()
+      createjs.Ticker.removeEventListener 'tick', @tick
+      @tick = null
+    stage
     
   uploadGenericPortrait: (callback) ->
     src = @getPortraitSource()
-    return unless src
+    return callback?() unless src
     src = src.replace('data:image/png;base64,', '').replace(/\ /g, '+')
     body =
       filename: 'portrait.png'
diff --git a/app/styles/play/level/hud.sass b/app/styles/play/level/hud.sass
index 981b9a096..674c77181 100644
--- a/app/styles/play/level/hud.sass
+++ b/app/styles/play/level/hud.sass
@@ -60,7 +60,7 @@
     .no-selection-message
       display: none
 
-    .thang-image-wrapper, .speaker-image-wrapper
+    .thang-canvas-wrapper, .speaker-image-wrapper
       width: 100px
       height: 100px
       border: 1px solid darkred
@@ -71,9 +71,11 @@
 
       &.team-humans
         border-color: darkred
+        background-color: rgba(255,100,100,0.5)
 
       &.team-ogres
         border-color: darkblue
+        background-color: rgba(100,100,255,0.5)
 
     .thang-props
       width: 144px
diff --git a/app/templates/play/level/hud.jade b/app/templates/play/level/hud.jade
index 56942a3d0..f3a67ff9b 100644
--- a/app/templates/play/level/hud.jade
+++ b/app/templates/play/level/hud.jade
@@ -2,8 +2,8 @@
 
 .center
 
-  .thang-image-wrapper.thang-elem
-    img.thang-image
+  .thang-canvas-wrapper.thang-elem
+    canvas.thang-canvas
   
   .thang-props.thang-elem
     .thang-name
@@ -17,9 +17,6 @@
         tbody
   
   .dialogue-area
-    .speaker-image-wrapper
-      canvas.thang-canvas(width=100, height=100)
-      img.speaker-image
     p.bubble.dialogue-bubble
 
   .no-selection-message
diff --git a/app/views/editor/thang/edit.coffee b/app/views/editor/thang/edit.coffee
index ae9966fdd..06091470d 100644
--- a/app/views/editor/thang/edit.coffee
+++ b/app/views/editor/thang/edit.coffee
@@ -166,7 +166,7 @@ module.exports = class ThangTypeEditView extends View
     @file = e.target.files[0]
     return unless @file
     return unless @file.type is 'text/javascript'
-    @$el.find('#upload-button').prop('disabled', true)
+#    @$el.find('#upload-button').prop('disabled', true)
     @reader = new FileReader()
     @reader.onload = @onFileLoad
     @reader.readAsText(@file)
@@ -301,7 +301,7 @@ module.exports = class ThangTypeEditView extends View
 
     res.success =>
       url = "/editor/thang/#{newThangType.get('slug') or newThangType.id}"
-      newThangType.uploadGenericPortrait =>
+      newThangType.uploadGenericPortrait ->
         document.location.href = url
 
   clearRawData: ->
diff --git a/app/views/play/level/hud_view.coffee b/app/views/play/level/hud_view.coffee
index dfcb51334..3d712974d 100644
--- a/app/views/play/level/hud_view.coffee
+++ b/app/views/play/level/hud_view.coffee
@@ -10,26 +10,26 @@ module.exports = class HUDView extends View
   template: template
   dialogueMode: false
 
-  constructor: (options) ->
-    @thangIDMap = {}
-    super options
-
   subscriptions:
     'surface:frame-changed': 'onFrameChanged'
+    'level-disable-controls': 'onDisableControls'
+    'level-enable-controls': 'onEnableControls'
     'surface:sprite-selected': 'onSpriteSelected'
     'sprite:speech-updated': 'onSpriteDialogue'
     'level-sprite-clear-dialogue': 'onSpriteClearDialogue'
-    'level-disable-controls': 'onDisableControls'
-    'level-enable-controls': 'onEnableControls'
     'level:shift-space-pressed': 'onShiftSpacePressed'
     'level:escape-pressed': 'onEscapePressed'
-    'god:new-world-created': 'onNewWorldCreated'
-    'surface:ticked': 'onTick'
     'dialogue-sound-completed': 'onDialogueSoundCompleted'
+    'thang-began-talking': 'onThangBeganTalking'
+    'thang-finished-talking': 'onThangFinishedTalking'
 
   events:
     'click': -> Backbone.Mediator.publish 'focus-editor'
 
+  afterRender: =>
+    super()
+    @$el.addClass 'no-selection'
+
   onFrameChanged: (e) ->
     @timeProgress = e.progress
     @update()
@@ -42,100 +42,83 @@ module.exports = class HUDView extends View
     return if e.controls and not ('hud' in e.controls)
     @disabled = false
 
-  onNewWorldCreated: (e) ->
-    @thangIDMap = {}
-    for thang in e.world.thangs
-      if @thang?.id is thang.id
-        #console.log('HUD updated thang for', thang.id)
-        @thang = thang
-        @createActions()
-      @thangIDMap[thang.id] = thang.spriteName
-
   onSpriteSelected: (e) ->
     # TODO: this allows the surface and HUD selection to get out of sync if we select another unit while in dialogue mode
     return if @disabled or @dialogueMode
     @switchToThangElements()
-    @setThang e.thang
+    @setThang e.thang, e.sprite?.thangType
 
   onSpriteDialogue: (e) ->
     return unless e.message
     spriteID = e.sprite.thang.id
-    spriteName = e.sprite.thangType?.get('name') or e.sprite.thang.spriteName
-    @setSpeaker spriteID, spriteName
-    @startAnimation spriteID
+    @setSpeaker e.sprite
+    @stage?.startTalking()
     @setMessage(e.message, e.mood, e.responses)
     window.tracker?.trackEvent 'Heard Sprite', {speaker: spriteID, message: e.message, label: e.message}, ['Google Analytics']
 
-  startAnimation: (spriteID) =>
-    @speakerStage.removeAllChildren()
-
-    #spriteData = spriteMap.dataForThang(spriteID)
-    spriteData = null  # we deleted SpriteMap, but haven't refactored to use vector animated portraits yet
-
-    canvas = $('canvas', @$el)
-    image = $('.speaker-image', @$el)
-    if spriteData?.sprite_data?.animations.portrait
-      image.hide()
-      canvas.show()
-    else
-      image.show()
-      canvas.hide()
-      return
-
   onDialogueSoundCompleted: ->
-    return unless @portraitSprite
-    @portraitSprite.gotoAndPlay('portrait_idle')
-
-  onTick: ->
-    @speakerStage.update()
+    @stage?.stopTalking()
 
   onSpriteClearDialogue: ->
     @clearSpeaker()
 
-  afterRender: =>
-    super()
-    @$el.addClass 'no-selection'
-    @speakerStage = new createjs.Stage($('canvas', @$el)[0])
-
-  setThang: (thang) ->
+  setThang: (thang, thangType) ->
     unless @speaker
       if not thang? and not @thang? then return
       if thang? and @thang? and thang.id is @thang.id then return
+        
     @thang = thang
+    @thangType = thangType
     @$el.toggleClass 'no-selection', not @thang?
     clearTimeout @hintNextSelectionTimeout
     @$el.find('.no-selection-message').hide()
     if not @thang
       @hintNextSelectionTimeout = _.delay((=> @$el.find('.no-selection-message').slideDown('slow')), 10000)
       return
-    @createAvatar @thang.id, @sprite
+    @createAvatar thangType
     @createProperties()
     @createActions()
     @update()
     @speaker = null
 
-  setSpeaker: (speaker, speakerType) ->
-    return if speaker is @speaker
-    image = @$el.find '.speaker-image'
-    spriteUtils.createAvatar @thangIDMap[speakerType] or speakerType, image
-    @speaker = speaker
+  setSpeaker: (speakerSprite) ->
+    return if speakerSprite is @speakerSprite
+    @speakerSprite = speakerSprite
+    @speaker = @speakerSprite.thang.id
+    @createAvatar @speakerSprite.thangType
     @$el.removeClass 'no-selection'
     @switchToDialogueElements()
 
   clearSpeaker: ->
     if not @thang
       @$el.addClass 'no-selection'
-    #console.log "clearSpeaker and have thang", @thang
     @setThang @thang
     @switchToThangElements()
     @speaker = null
+    @speakerSprite = null
     @bubble = null
     @update()
 
-  createAvatar: (id) ->
-    image = @$el.find '.thang-image'
-    spriteUtils.createAvatar @thangIDMap[id] or id, image
-    image.attr('title', id).parent().removeClass('team-ogres').removeClass('team-humans').addClass('team-' + @thang.team)
+  createAvatar: (thangType) ->
+    stage = thangType.getPortraitStage()
+    wrapper = @$el.find '.thang-canvas-wrapper'
+    newCanvas = $(stage.canvas).addClass('thang-canvas')
+    wrapper.empty().append(newCanvas)
+    team = @thang?.team or @speakerSprite?.thang?.team
+    newCanvas.parent().removeClass('team-ogres').removeClass('team-humans').addClass("team-#{team}")
+    stage.update()
+    @stage?.stopTalking()
+    @stage = stage
+    f = => console.log 'new canvas style timeout', newCanvas.attr 'style'
+    setTimeout f, 1000
+
+  onThangBeganTalking: (e) ->
+    return unless @stage and @thang is e.thang
+    @stage?.startTalking()
+
+  onThangFinishedTalking: (e) ->
+    return unless @stage and @thang is e.thang
+    @stage?.stopTalking()
 
   createProperties: ->
     props = @$el.find('.thang-props')
@@ -219,6 +202,7 @@ module.exports = class HUDView extends View
   switchToDialogueElements: ->
     @dialogueMode = true
     $('.thang-elem', @$el).addClass('hide')
+    @$el.find('.thang-canvas-wrapper').removeClass('hide')
     $('.dialogue-area', @$el)
       .removeClass('hide')
       .animate({opacity:1.0}, 200)
@@ -235,11 +219,8 @@ module.exports = class HUDView extends View
 
   update: ->
     return unless @thang and not @speaker
-    # Update avatar?
-
     # Update properties
     @updatePropElement(prop, @thang[prop]) for prop in @thang.hudProperties ? []
-
     # Update action timeline
     @updateActions()
 
@@ -343,3 +324,7 @@ module.exports = class HUDView extends View
       timeline.append bar
 
     ael
+
+  destroy: ->
+    super()
+    @stage?.stopTalking()
diff --git a/server/file.coffee b/server/file.coffee
index e1c9ed1a2..ddeb896b1 100644
--- a/server/file.coffee
+++ b/server/file.coffee
@@ -119,6 +119,7 @@ checkExistence = (options, res, force, done) ->
       returnConflict(res)
       done(true)
     else if files.length
+      q = { _id: files[0]._id }
       q.root = 'media'
       Grid.gfs.remove q, (err) ->
         return returnServerError(res) if err