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