CocoClass = require 'core/CocoClass' {createProgressBar} = require './sprite_utils' Camera = require './Camera' Mark = require './Mark' Label = require './Label' AudioPlayer = require 'lib/AudioPlayer' {me} = require 'core/auth' # We'll get rid of this once level's teams actually have colors healthColors = ogres: [64, 128, 212] humans: [255, 0, 0] neutral: [64, 212, 128] # Sprite: EaselJS-based view/controller for Thang model module.exports = Lank = class Lank extends CocoClass thangType: null # ThangType instance sprite: null healthBar: null marks: null labels: null ranges: null options: groundLayer: null textLayer: null floatingLayer: null thang: null camera: null showInvisible: false preloadSounds: true possessed: false flipped: false flippedCount: 0 actionQueue: null actions: null rotation: 0 # Scale numbers scaleFactorX: 1 # Current scale adjustment. This can change rapidly. scaleFactorY: 1 targetScaleFactorX: 1 # What the scaleFactor is going toward during a tween. targetScaleFactorY: 1 # ACTION STATE # Actions have relations. If you say 'move', 'move_side' may play because of a direction # relationship, and if you say 'cast', 'cast_begin' may happen first, or 'cast_end' after. currentRootAction: null # action that, in general, is playing or will play currentAction: null # related action that is right now playing subscriptions: 'level:sprite-dialogue': 'onDialogue' 'level:sprite-clear-dialogue': 'onClearDialogue' 'level:set-letterbox': 'onSetLetterbox' 'surface:ticked': 'onSurfaceTicked' 'sprite:move': 'onMove' constructor: (@thangType, options) -> super() spriteName = @thangType.get('name') @isMissile = /(Missile|Arrow|Spear|Bolt)/.test(spriteName) and not /(Tower|Charge)/.test(spriteName) @options = _.extend($.extend(true, {}, @options), options) @setThang @options.thang if @thang? options = @thang?.getLankOptions?() @options.colorConfig = options.colorConfig if options and options.colorConfig console.error @toString(), 'has no ThangType!' unless @thangType # this is a stub, use @setSprite to swap it out for something else later @sprite = new createjs.Container @actionQueue = [] @marks = {} @labels = {} @ranges = [] @handledDisplayEvents = {} @age = 0 @stillLoading = true if @thangType.isFullyLoaded() then @onThangTypeLoaded() else @listenToOnce(@thangType, 'sync', @onThangTypeLoaded) toString: -> "" onThangTypeLoaded: -> @stillLoading = false if @options.preloadSounds for trigger, sounds of @thangType.get('soundTriggers') or {} when trigger isnt 'say' AudioPlayer.preloadSoundReference sound for sound in sounds when sound if @thangType.get('raster') @actions = {} @isRaster = true else @actions = @thangType.getActions() @createMarks() @scaleFactorX = @thang.scaleFactorX if @thang?.scaleFactorX? @scaleFactorX = @thang.scaleFactor if @thang?.scaleFactor? @scaleFactorY = @thang.scaleFactorY if @thang?.scaleFactorY? @scaleFactorY = @thang.scaleFactor if @thang?.scaleFactor? @updateAction() unless @currentAction setSprite: (newSprite) -> if @sprite @sprite.off 'animationend', @playNextAction @sprite.destroy?() if parent = @sprite.parent parent.removeChild @sprite if parent.spriteSheet is newSprite.spriteSheet parent.addChild newSprite # get the lank to update things for prop in ['lastPos', 'currentRootAction'] delete @[prop] @sprite = newSprite if @thang and @thang.stateChanged is false @thang.stateChanged = true @configureMouse() @sprite.on 'animationend', @playNextAction @playAction(@currentAction) if @currentAction and not @stillLoading @trigger 'new-sprite', @sprite ################################################## # QUEUEING AND PLAYING ACTIONS queueAction: (action) -> # The normal way to have an action play action = @actions[action] if _.isString(action) action ?= @actions.idle @actionQueue = [] @actionQueue.push @currentRootAction.relatedActions.end if @currentRootAction?.relatedActions?.end @actionQueue.push action.relatedActions.begin if action.relatedActions?.begin @actionQueue.push action if action.goesTo and nextAction = @actions[action.goesTo] @actionQueue.push nextAction if nextAction @currentRootAction = action @playNextAction() onSurfaceTicked: (e) -> @age += e.dt playNextAction: => return if @destroyed @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 or action.goesTo @show() return @updateActionDirection() unless action.animation or action.container or action.goesTo return if @sprite.placeholder m = if action.container then 'gotoAndStop' else 'gotoAndPlay' @sprite[m]?(action.name) @updateScale() @updateRotation() hide: -> @hiding = true @updateAlpha() show: -> @hiding = false @updateAlpha() stop: -> @sprite?.stop?() mark.stop() for name, mark of @marks @stopped = true play: -> @sprite?.play?() mark.play() for name, mark of @marks @stopped = false update: (frameChanged) -> # Gets the sprite to reflect what the current state of the thangs and surface are return false if @stillLoading thangUnchanged = @thang and @thang.stateChanged is false if (frameChanged and not thangUnchanged) or (@thang and @thang.bobHeight) or @notOfThisWorld @updatePosition() return false if thangUnchanged frameChanged = frameChanged or @targetScaleFactorX isnt @scaleFactorX or @targetScaleFactorY isnt @scaleFactorY if frameChanged @handledDisplayEvents = {} @updateScale() # must happen before rotation @updateAlpha() @updateRotation() @updateAction() @updateStats() @updateGold() @showAreaOfEffects() @showTextEvents() @updateHealthBar() @updateMarks() @updateLabels() @thang.stateChanged = false if @thang and @thang.stateChanged is true return true showAreaOfEffects: -> return unless @thang?.currentEvents for event in @thang.currentEvents continue unless _.string.startsWith event, 'aoe-' continue if @handledDisplayEvents[event] @handledDisplayEvents[event] = true args = JSON.parse(event[4...]) key = 'aoe-' + JSON.stringify(args[2..]) layerName = args[6] ? 'ground' # Can also specify 'floating'. unless layer = @options[layerName + 'Layer'] console.error "#{@thang.id} couldn't find layer #{layerName}Layer for AOE effect #{key}; using ground layer." layer = @options.groundLayer unless key in layer.spriteSheet.getAnimations() circle = new createjs.Shape() radius = args[2] * Camera.PPM if args.length is 4 circle.graphics.beginFill(args[3]).drawCircle(0, 0, radius) else startAngle = args[4] or 0 endAngle = args[5] or 2 * Math.PI if startAngle is endAngle startAngle = 0 endAngle = 2 * Math.PI circle.graphics.beginFill(args[3]) .lineTo(0, 0) .lineTo(radius * Math.cos(startAngle), radius * Math.sin(startAngle)) .arc(0, 0, radius, startAngle, endAngle) .lineTo(0, 0) layer.addCustomGraphic(key, circle, [-radius, -radius, radius*2, radius*2]) circle = new createjs.Sprite(layer.spriteSheet) circle.gotoAndStop(key) pos = @options.camera.worldToSurface {x: args[0], y: args[1]} circle.x = pos.x circle.y = pos.y resFactor = layer.resolutionFactor circle.scaleY = @options.camera.y2x * 0.7 / resFactor circle.scaleX = 0.7 / resFactor circle.alpha = 0.2 layer.addChild circle createjs.Tween.get(circle) .to({alpha: 0.6, scaleY: @options.camera.y2x / resFactor, scaleX: 1 / resFactor}, 100, createjs.Ease.circOut) .to({alpha: 0, scaleY: 0, scaleX: 0}, 700, createjs.Ease.circIn) .call => return if @destroyed layer.removeChild circle delete @handledDisplayEvents[event] showTextEvents: -> return unless @thang?.currentEvents for event in @thang.currentEvents continue unless _.string.startsWith event, 'text-' continue if @handledDisplayEvents[event] @handledDisplayEvents[event] = true options = JSON.parse(event[5...]) label = new createjs.Text options.text, "bold #{options.size or 16}px Arial", options.color or '#FFF' shadowColor = {humans: '#F00', ogres: '#00F', neutral: '#0F0', common: '#0F0'}[@thang.team] ? '#000' label.shadow = new createjs.Shadow shadowColor, 1, 1, 3 offset = @getOffset 'aboveHead' [label.x, label.y] = [@sprite.x + offset.x - label.getMeasuredWidth() / 2, @sprite.y + offset.y] @options.textLayer.addChild label window.labels ?= [] window.labels.push label label.alpha = 0 createjs.Tween.get(label) .to({y: label.y-2, alpha: 1}, 200, createjs.Ease.linear) .to({y: label.y-12}, 1000, createjs.Ease.linear) .to({y: label.y-22, alpha: 0}, 1000, createjs.Ease.linear) .call => return if @destroyed @options.textLayer.removeChild label getBobOffset: -> return 0 unless @thang.bobHeight return @lastBobOffset if @stopped return @lastBobOffset = @thang.bobHeight * (1 + Math.sin(@age * Math.PI / @thang.bobTime)) getWorldPosition: -> p1 = if @possessed then @shadow.pos else @thang.pos if bobOffset = @getBobOffset() p1 = p1.copy?() or _.clone(p1) p1.z += bobOffset x: p1.x, y: p1.y, z: if @thang.isLand then 0 else p1.z - @thang.depth / 2 updatePosition: (whileLoading=false) -> return if @stillLoading and not whileLoading return unless @thang?.pos and @options.camera? [p0, p1] = [@lastPos, @thang.pos] return if p0 and p0.x is p1.x and p0.y is p1.y and p0.z is p1.z and not @thang.bobHeight wop = @getWorldPosition() sup = @options.camera.worldToSurface wop [@sprite.x, @sprite.y] = [sup.x, sup.y] @lastPos = p1.copy?() or _.clone(p1) unless whileLoading @hasMoved = true if @thangType.get('name') is 'Flag' and not @notOfThisWorld # Let the pending flags know we're here (but not this call stack, they need to delete themselves, and we may be iterating sprites). _.defer => Backbone.Mediator.publish 'surface:flag-appeared', sprite: @ updateScale: (force) -> return unless @sprite if @thangType.get('matchWorldDimensions') and @thang and @options.camera if force or @thang.width isnt @lastThangWidth or @thang.height isnt @lastThangHeight or @thang.rotation isnt @lastThangRotation bounds = @sprite.getBounds() return unless bounds @sprite.scaleX = @thang.width * Camera.PPM / bounds.width * (@options.camera.y2x + (1 - @options.camera.y2x) * Math.abs Math.cos @thang.rotation) @sprite.scaleY = @thang.height * Camera.PPM / bounds.height * (@options.camera.y2x + (1 - @options.camera.y2x) * Math.abs Math.sin @thang.rotation) @sprite.regX = bounds.width * 3 / 4 # Why not / 2? I don't know. @sprite.regY = bounds.height * 3 / 4 # Why not / 2? I don't know. unless @thang.spriteName is 'Beam' @sprite.scaleX *= @thangType.get('scale') ? 1 @sprite.scaleY *= @thangType.get('scale') ? 1 [@lastThangWidth, @lastThangHeight, @lastThangRotation] = [@thang.width, @thang.height, @thang.rotation] return scaleX = scaleY = 1 if @isMissile # Scales the arrow so it appears longer when flying parallel to horizon. # To do that, we convert angle to [0, 90] (mirroring half-planes twice), then make linear function out of it: # (a - x) / a: equals 1 when x = 0, equals 0 when x = a, monotonous in between. That gives us some sort of # degenerative multiplier. # For our purposes, a = 90 - the direction straight upwards. # Then we use r + (1 - r) * x function with r = 0.5, so that # maximal scale equals 1 (when x is at it's maximum) and minimal scale is 0.5. # Notice that the value of r is empirical. angle = @getRotation() angle = -angle if angle < 0 angle = 180 - angle if angle > 90 scaleX = 0.5 + 0.5 * (90 - angle) / 90 # console.error 'No thang for', @ unless @thang @sprite.scaleX = @sprite.baseScaleX * @scaleFactorX * scaleX @sprite.scaleY = @sprite.baseScaleY * @scaleFactorY * scaleY newScaleFactorX = @thang?.scaleFactorX ? @thang?.scaleFactor ? 1 newScaleFactorY = @thang?.scaleFactorY ? @thang?.scaleFactor ? 1 if @layer?.name is 'Land' or @thang?.spriteName is 'Beam' @scaleFactorX = newScaleFactorX @scaleFactorY = newScaleFactorY else if @thang and (newScaleFactorX isnt @targetScaleFactorX or newScaleFactorY isnt @targetScaleFactorY) @targetScaleFactorX = newScaleFactorX @targetScaleFactorY = newScaleFactorY createjs.Tween.removeTweens(@) createjs.Tween.get(@).to({scaleFactorX: @targetScaleFactorX, scaleFactorY: @targetScaleFactorY}, 2000, createjs.Ease.elasticOut) updateAlpha: -> @sprite.alpha = if @hiding then 0 else 1 return unless @thang?.alpha? return if @sprite.alpha is @thang.alpha @sprite.alpha = @thang.alpha if @options.showInvisible @sprite.alpha = Math.max 0.5, @sprite.alpha mark.updateAlpha @thang.alpha for name, mark of @marks @healthBar?.alpha = @thang.alpha updateRotation: (sprite) -> rotationType = @thangType.get('rotationType') return if rotationType is 'fixed' rotation = @getRotation() if @isMissile and @thang.velocity # Rotates the arrow to see it arc based on velocity.z. # Notice that rotation here does not affect thang's state - it is just the effect. # Thang's rotation is always pointing where it is heading. vz = @thang.velocity.z if vz and speed = @thang.velocity.magnitude(true) vx = @thang.velocity.x heading = @thang.velocity.heading() xFactor = Math.cos heading zFactor = vz / Math.sqrt(vz * vz + vx * vx) rotation -= xFactor * zFactor * 45 sprite ?= @sprite return sprite.rotation = rotation if rotationType is 'free' or not rotationType @updateIsometricRotation(rotation, sprite) getRotation: -> thang = if @possessed then @shadow else @thang return @rotation if not thang?.rotation rotation = thang?.rotation rotation = (360 - (rotation * 180 / Math.PI) % 360) % 360 rotation -= 360 if rotation > 180 rotation updateIsometricRotation: (rotation, sprite) -> return unless @currentAction return if _.string.endsWith(@currentAction.name, 'back') return if _.string.endsWith(@currentAction.name, 'fore') sprite.scaleX *= -1 if Math.abs(rotation) >= 90 ################################################## updateAction: -> return if @isRaster or @actionLocked action = @determineAction() isDifferent = action isnt @currentRootAction or action is null if not action and @thang?.actionActivated and not @stopLogging console.error 'action is', action, 'for', @thang?.id, 'from', @currentRootAction, @thang.action, @thang.getActionName?() @stopLogging = true @queueAction(action) if action and (isDifferent or (@thang?.actionActivated and action.name isnt 'move')) @updateActionDirection() determineAction: -> action = null thang = if @possessed then @shadow else @thang action = thang.action if thang?.acts action ?= @currentRootAction.name if @currentRootAction? action ?= 'idle' unless @actions[action]? @warnedFor ?= {} console.warn 'Cannot show action', action, 'for', @thangType.get('name'), 'because it DNE' unless @warnedFor[action] @warnedFor[action] = true return if @action is 'idle' then null else 'idle' #action = 'break' if @actions.break? and @thang?.erroredOut # This makes it looks like it's dead when it's not: bad in Brawlwood. action = 'die' if @actions.die? and thang?.health? and thang.health <= 0 @actions[action] updateActionDirection: (@wallGrid=null) -> # wallGrid is only needed for wall grid face updates; should refactor if this works return unless action = @getActionDirection() @playAction(action) if action isnt @currentAction lockAction: -> (@actionLocked=true) getActionDirection: (rootAction=null) -> rootAction ?= @currentRootAction return null unless relatedActions = rootAction?.relatedActions ? {} rotation = @getRotation() if relatedActions['111111111111'] # has grid-surrounding-wall-based actions if @wallGrid @hadWallGrid = true action = '' tileSize = 4 [gx, gy] = [@thang.pos.x, @thang.pos.y] for y in [gy + tileSize, gy, gy - tileSize, gy - tileSize * 2] for x in [gx - tileSize, gx, gx + tileSize] if x >= 0 and y >= 0 and x < @wallGrid.width and y < @wallGrid.height wallThangs = @wallGrid.contents x, y else wallThangs = ['outside of the map yo'] if wallThangs.length is 0 if y is gy and x is gx action += '1' # the center wall we're placing else action += '0' else if wallThangs.length is 1 action += '1' else console.error 'Overlapping walls at', x, y, '...', wallThangs action += '1' matchedAction = '111111111111' for relatedAction of relatedActions if action.match(relatedAction.replace(/\?/g, '.')) matchedAction = relatedAction break #console.log 'returning', matchedAction, 'for', @thang.id, 'at', gx, gy return relatedActions[matchedAction] else if @hadWallGrid return null else keys = _.keys relatedActions index = Math.max 0, Math.floor((179 + rotation) / 360 * keys.length) #console.log 'Showing', relatedActions[keys[index]] return relatedActions[keys[index]] value = Math.abs(rotation) direction = null direction = 'side' if value <= 45 or value >= 135 direction = 'fore' if 135 > rotation > 45 direction = 'back' if -135 < rotation < -45 relatedActions[direction] updateStats: -> return unless @thang and @thang.health isnt @lastHealth @lastHealth = @thang.health if bar = @healthBar healthPct = Math.max(@thang.health / @thang.maxHealth, 0) bar.scaleX = healthPct / @options.floatingLayer.resolutionFactor if @thang.showsName @setNameLabel(if @thang.health <= 0 then '' else @thang.id) else if @options.playerName @setNameLabel @options.playerName configureMouse: -> @sprite.cursor = 'pointer' if @thang?.isSelectable @sprite.mouseEnabled = @sprite.mouseChildren = false unless @thang?.isSelectable or @thang?.isLand if @sprite.mouseEnabled @sprite.on 'mousedown', @onMouseEvent, @, false, 'sprite:mouse-down' @sprite.on 'click', @onMouseEvent, @, false, 'sprite:clicked' @sprite.on 'dblclick', @onMouseEvent, @, false, 'sprite:double-clicked' @sprite.on 'pressmove', @onMouseEvent, @, false, 'sprite:dragged' @sprite.on 'pressup', @onMouseEvent, @, false, 'sprite:mouse-up' onMouseEvent: (e, ourEventName) -> return if @letterboxOn or not @sprite p = @sprite p = p.parent while p.parent newEvent = sprite: @, thang: @thang, originalEvent: e, canvas: p.canvas @trigger ourEventName, newEvent Backbone.Mediator.publish ourEventName, newEvent addHealthBar: -> return unless @thang?.health? and 'health' in (@thang?.hudProperties ? []) and @options.floatingLayer team = @thang?.team or 'neutral' key = "#{team}-health-bar" unless key in @options.floatingLayer.spriteSheet.getAnimations() healthColor = healthColors[team] bar = createProgressBar(healthColor) @options.floatingLayer.addCustomGraphic(key, bar, bar.bounds) hadHealthBar = @healthBar @healthBar = new createjs.Sprite(@options.floatingLayer.spriteSheet) @healthBar.gotoAndStop(key) offset = @getOffset 'aboveHead' @healthBar.scaleX = @healthBar.scaleY = 1 / @options.floatingLayer.resolutionFactor @healthBar.name = 'health bar' @options.floatingLayer.addChild @healthBar @updateHealthBar() @lastHealth = null if not hadHealthBar @listenTo @options.floatingLayer, 'new-spritesheet', @addHealthBar getActionProp: (prop, subProp, def=null) -> # Get a property or sub-property from an action, falling back to ThangType for val in [@currentAction?[prop], @thangType.get(prop)] val = val[subProp] if val? and subProp return val if val? def getOffset: (prop) -> # Get the proper offset from either the current action or the ThangType 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 if not @isRaster scale = @getActionProp 'scale', null, 1 scale *= @sprite.parent.resolutionFactor if prop is 'registration' pos.x *= scale pos.y *= scale if @thang and prop isnt 'registration' pos.x *= @thang.scaleFactorX ? @thang.scaleFactor ? 1 pos.y *= @thang.scaleFactorY ? @thang.scaleFactor ? 1 # We might need to do this, but I don't have a good test case yet. TODO: figure out. #if prop isnt @registration # pos.x *= if @getActionProp 'flipX' then -1 else 1 # pos.y *= if @getActionProp 'flipY' then -1 else 1 pos createMarks: -> return unless @options.camera if @thang # TODO: Add back ranges # allProps = [] # allProps = allProps.concat (@thang.hudProperties ? []) # allProps = allProps.concat (@thang.programmableProperties ? []) # allProps = allProps.concat (@thang.moreProgrammableProperties ? []) # # for property in allProps # if m = property.match /.*(Range|Distance|Radius)$/ # if @thang[m[0]]? and @thang[m[0]] < 9001 # @ranges.push # name: m[0] # radius: @thang[m[0]] # # @ranges = _.sortBy @ranges, 'radius' # @ranges.reverse() # # @addMark range.name for range in @ranges # TODO: add back bounds # @addMark('bounds').toggle true if @thang?.drawsBounds @addMark('shadow').toggle true unless @thangType.get('shadow') is 0 updateMarks: -> return unless @options.camera @addMark 'repair', null, 'repair' if @thang?.erroredOut @marks.repair?.toggle @thang?.erroredOut if @selected @marks[range['name']].toggle true for range in @ranges else @marks[range['name']].toggle false for range in @ranges if @isMissile and @thang.action is 'die' @marks.shadow?.hide() mark.update() for name, mark of @marks #@thang.effectNames = ['warcry', 'confuse', 'control', 'curse', 'fear', 'poison', 'paralyze', 'regen', 'sleep', 'slow', 'haste'] @updateEffectMarks() if @thang?.effectNames?.length or @previousEffectNames?.length updateEffectMarks: -> return if _.isEqual @thang.effectNames, @previousEffectNames return if @stopped for effect in @thang.effectNames mark = @addMark effect, @options.floatingLayer, effect mark.statusEffect = true mark.toggle 'on' mark.show() if @previousEffectNames for effect in @previousEffectNames continue if effect in @thang.effectNames mark = @marks[effect] mark.toggle false if @thang.effectNames.length > 1 and not @effectInterval @rotateEffect() @effectInterval = setInterval @rotateEffect, 1500 else if @effectInterval and @thang.effectNames.length <= 1 clearInterval @effectInterval @effectInterval = null @previousEffectNames = @thang.effectNames rotateEffect: => effects = (m.name for m in _.values(@marks) when m.on and m.statusEffect and m.mark) return unless effects.length effects.sort() @effectIndex ?= 0 @effectIndex = (@effectIndex + 1) % effects.length @marks[effect].hide() for effect in effects @marks[effects[@effectIndex]].show() setHighlight: (to, delay) -> @addMark 'highlight', @options.floatingLayer, 'highlight' if to @marks.highlight?.highlightDelay = delay @marks.highlight?.toggle to and not @dimmed setDimmed: (@dimmed) -> @marks.highlight?.toggle @marks.highlight.on and not @dimmed setThang: (@thang) -> @options.thang = @thang setDebug: (debug) -> return unless @thang?.collides and @options.camera? @addMark 'debug', @options.floatingLayer if debug if d = @marks.debug d.toggle debug d.updatePosition() addLabel: (name, style) -> @labels[name] ?= new Label sprite: @, camera: @options.camera, layer: @options.textLayer, style: style @labels[name] addMark: (name, layer, thangType=null) -> @marks[name] ?= new Mark name: name, lank: @, camera: @options.camera, layer: layer ? @options.groundLayer, thangType: thangType @marks[name] notifySpeechUpdated: (e) -> e = _.clone(e) e.sprite = @ e.blurb ?= '...' e.thang = @thang Backbone.Mediator.publish 'sprite:speech-updated', e isTalking: -> Boolean @labels.dialogue?.text or @labels.say?.text onDialogue: (e) -> return unless @thang?.id is e.spriteID unless @thang?.id is 'Hero Placeholder' # Don't show these for heroes, because they aren't actually first-person, just LevelDialogueView narration label = @addLabel 'dialogue', Label.STYLE_DIALOGUE label.setText e.blurb or '...' sound = e.sound ? AudioPlayer.soundForDialogue e.message, @thangType.get 'soundTriggers' @dialogueSoundInstance?.stop() if @dialogueSoundInstance = @playSound sound, false @dialogueSoundInstance.addEventListener 'complete', -> Backbone.Mediator.publish 'sprite:dialogue-sound-completed', {} @notifySpeechUpdated e onClearDialogue: (e) -> return unless @labels.dialogue?.text @labels.dialogue?.setText null @dialogueSoundInstance?.stop() @notifySpeechUpdated {} onSetLetterbox: (e) -> @letterboxOn = e.on setNameLabel: (name) -> label = @addLabel 'name', Label.STYLE_NAME label.setText name updateLabels: -> return unless @thang blurb = if @thang.health <= 0 then null else @thang.sayMessage # Dead men tell no tales blurb = null if blurb in ['For Thoktar!', 'Bones!', 'Behead!', 'Destroy!', 'Die, humans!'] # Let's just hear, not see, these ones. if /Hero Placeholder/.test(@thang.id) labelStyle = Label.STYLE_DIALOGUE else labelStyle = @thang.labelStyle ? Label.STYLE_SAY @addLabel 'say', labelStyle if blurb if @labels.say?.setText blurb @notifySpeechUpdated blurb: blurb label.update() for name, label of @labels updateGold: -> # TODO: eventually this should be moved into some sort of team-based update # rather than an each-thang-that-shows-gold-per-team thing. return unless @thang return if @thang.gold is @lastGold gold = Math.floor @thang.gold ? 0 if @thang.world.age is 0 gold = @thang.world.initialTeamGold[@thang.team].gold return if gold is @lastGold @lastGold = gold Backbone.Mediator.publish 'surface:gold-changed', {team: @thang.team, gold: gold, goldEarned: Math.floor(@thang.goldEarned ? 0)} shouldMuteMessage: (m) -> if me.getAnnouncesActionAudioGroup() in ['no-audio', 'just-take-damage'] return true if m in ['moveRight', 'moveUp', 'moveDown', 'moveLeft'] return true if /^attack /.test m return true if /^Repeating loop/.test m return true if /^findNearestEnemy/.test m return false if m in ['moveRight', 'moveUp', 'moveDown', 'moveLeft'] @previouslySaidMessages ?= {} t0 = @previouslySaidMessages[m] ? 0 t1 = new Date() @previouslySaidMessages[m] = t1 return true if t1 - t0 < 5 * 1000 false playSounds: (withDelay=true, volume=1.0) -> for event in @thang.currentEvents ? [] if event is 'take-damage' and me.getAnnouncesActionAudioGroup() in ['no-audio', 'without-take-damage'] null # Skip playing it else @playSound event, withDelay, volume if event is 'pay-bounty-gold' and @thang.bountyGold > 25 and @thang.team isnt me.team AudioPlayer.playInterfaceSound 'coin_1', 0.25 if @thang.actionActivated and (action = @thang.getActionName()) isnt 'say' @playSound action, withDelay, volume if @thang.sayMessage and withDelay and not @thang.silent and not @shouldMuteMessage @thang.sayMessage # don't play sayMessages while scrubbing, annoying offsetFrames = Math.abs(@thang.sayStartTime - @thang.world.age) / @thang.world.dt if offsetFrames <= 2 # or (not withDelay and offsetFrames < 30) sound = AudioPlayer.soundForDialogue @thang.sayMessage, @thangType.get 'soundTriggers' @playSound sound, false, volume playSound: (sound, withDelay=true, volume=1.0) -> if _.isString sound sound = @thangType.get('soundTriggers')?[sound] if _.isArray sound sound = sound[Math.floor Math.random() * sound.length] return null unless sound delay = if withDelay and sound.delay then 1000 * sound.delay / createjs.Ticker.getFPS() else 0 name = AudioPlayer.nameForSoundReference sound AudioPlayer.preloadSoundReference sound instance = AudioPlayer.playSound name, volume, delay, @getWorldPosition() #console.log @thang?.id, 'played sound', name, 'with delay', delay, 'volume', volume, 'and got sound instance', instance instance onMove: (e) -> return unless e.spriteID is @thang?.id pos = e.pos if _.isArray pos pos = new Vector pos... else if _.isString pos return console.warn 'Couldn\'t find target sprite', pos, 'from', @options.sprites unless pos of @options.sprites target = @options.sprites[pos].thang heading = Vector.subtract(target.pos, @thang.pos).normalize() distance = @thang.pos.distance target.pos offset = Math.max(target.width, target.height, 2) / 2 + 3 pos = Vector.add(@thang.pos, heading.multiply(distance - offset)) Backbone.Mediator.publish 'level:sprite-clear-dialogue', {} @onClearDialogue() args = [pos] args.push(e.duration) if e.duration? @move(args...) move: (pos, duration=2000, endAnimation='idle') => @updateShadow() if not duration createjs.Tween.removeTweens(@shadow.pos) if @lastTween @lastTween = null z = @shadow.pos.z @shadow.pos = pos @shadow.pos.z = z @sprite.gotoAndPlay?(endAnimation) return @shadow.action = 'move' @shadow.actionActivated = true @pointToward(pos) @possessed = true @update true ease = createjs.Ease.getPowInOut(2.2) if @lastTween ease = createjs.Ease.getPowOut(1.2) createjs.Tween.removeTweens(@shadow.pos) endFunc = => @lastTween = null @sprite.gotoAndPlay(endAnimation) unless @stillLoading @shadow.action = 'idle' @update true @possessed = false @lastTween = createjs.Tween .get(@shadow.pos) .to({x: pos.x, y: pos.y}, duration, ease) .call(endFunc) pointToward: (pos) -> @shadow.rotation = Math.atan2(pos.y - @shadow.pos.y, pos.x - @shadow.pos.x) if (@shadow.rotation * 180 / Math.PI) % 90 is 0 @shadow.rotation += 0.01 updateShadow: -> @shadow = {} if not @shadow @shadow.pos = @thang.pos @shadow.rotation = @thang.rotation @shadow.action = @thang.action @shadow.actionActivated = @thang.actionActivated updateHealthBar: -> return unless @healthBar bounds = @healthBar.getBounds() offset = @getOffset 'aboveHead' @healthBar.x = @sprite.x - (-offset.x + bounds.width / 2 / @options.floatingLayer.resolutionFactor) @healthBar.y = @sprite.y - (-offset.y + bounds.height / 2 / @options.floatingLayer.resolutionFactor) destroy: -> mark.destroy() for name, mark of @marks label.destroy() for name, label of @labels p.removeChild @healthBar if p = @healthBar?.parent @sprite?.off 'animationend', @playNextAction clearInterval @effectInterval if @effectInterval @dialogueSoundInstance?.removeAllEventListeners() super()