codecombat/app/lib/surface/Mark.coffee

346 lines
12 KiB
CoffeeScript

CocoClass = require 'lib/CocoClass'
Camera = require './Camera'
ThangType = require 'models/ThangType'
markThangTypes = {}
module.exports = class Mark extends CocoClass
subscriptions: {}
alpha: 1
constructor: (options) ->
super()
options ?= {}
@name = options.name
@lank = options.lank
@camera = options.camera
@layer = options.layer
@thangType = options.thangType
@listenTo @layer, 'new-spritesheet', @onLayerMadeSpriteSheet
console.error @toString(), 'needs a name.' unless @name
console.error @toString(), 'needs a camera.' unless @camera
console.error @toString(), 'needs a layer.' unless @layer
@build()
destroy: ->
createjs.Tween.removeTweens @sprite if @sprite
@sprite?.parent?.removeChild @sprite
if @markLank
@layer.removeLank(@markLank)
@markLank.destroy()
@lank = null
super()
toString: -> "<Mark #{@name}: Sprite #{@lank?.thang?.id ? 'None'}>"
onLayerMadeSpriteSheet: ->
return unless @sprite
# rebuild sprite for new sprite sheet
@layer.removeChild @sprite
@sprite = null
@build()
@layer.addChild @sprite
@layer.updateLayerOrder()
# @updatePosition()
@update()
toggle: (to) ->
to = !!to
return @ if to is @on
return @toggleTo = to unless @sprite
@on = to
delete @toggleTo
if @on
if @markLank
@layer.addLank(@markLank)
else
@layer.addChild @sprite
@layer.updateLayerOrder()
else
if @markLank
@layer.removeLank(@markLank)
else
@layer.removeChild @sprite
if @highlightTween
@highlightDelay = @highlightTween = null
createjs.Tween.removeTweens @sprite
@sprite.visible = true
@
setLayer: (layer) ->
return if layer is @layer
wasOn = @on
@toggle false
@layer = layer
@toggle true if wasOn
setLank: (lank) ->
return if lank is @lank
@lank = lank
@build()
@
build: ->
unless @sprite
if @name is 'bounds' then @buildBounds()
else if @name is 'shadow' then @buildShadow()
else if @name is 'debug' then @buildDebug()
else if @name.match(/.+(Range|Distance|Radius)$/) then @buildRadius(@name)
else if @thangType then @buildSprite()
else console.error 'Don\'t know how to build mark for', @name
@sprite?.mouseEnabled = false
@
buildBounds: ->
@sprite = new createjs.Container()
@sprite.mouseChildren = false
style = @lank.thang.drawsBoundsStyle
@drawsBoundsIndex = @lank.thang.drawsBoundsIndex
return if style is 'corner-text' and @lank.thang.world.age is 0
# Confusingly make some semi-random colors that'll be consistent based on the drawsBoundsIndex
colors = (128 + Math.floor(('0.'+Math.sin(3 * @drawsBoundsIndex + i).toString().substr(6)) * 128) for i in [1 ... 4])
color = "rgba(#{colors[0]}, #{colors[1]}, #{colors[2]}, 0.5)"
[w, h] = [@lank.thang.width * Camera.PPM, @lank.thang.height * Camera.PPM * @camera.y2x]
if style in ['border-text', 'corner-text']
@drawsBoundsBorderShape = shape = new createjs.Shape()
shape.graphics.setStrokeStyle 5
shape.graphics.beginStroke color
if style is 'border-text'
shape.graphics.beginFill color.replace('0.5', '0.25')
else
shape.graphics.beginFill color
if @lank.thang.shape in ['ellipsoid', 'disc']
shape.drawEllipse 0, 0, w, h
else
shape.graphics.drawRect -w / 2, -h / 2, w, h
shape.graphics.endStroke()
shape.graphics.endFill()
@sprite.addChild shape
if style is 'border-text'
text = new createjs.Text '' + @drawsBoundsIndex, '20px Arial', color.replace('0.5', '1')
text.regX = text.getMeasuredWidth() / 2
text.regY = text.getMeasuredHeight() / 2
text.shadow = new createjs.Shadow('#000000', 1, 1, 0)
@sprite.addChild text
else if style is 'corner-text'
return if @lank.thang.world.age is 0
letter = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ'[@drawsBoundsIndex % 26]
text = new createjs.Text letter, '14px Arial', '#333333' # color.replace('0.5', '1')
text.x = -w / 2 + 2
text.y = -h / 2 + 2
@sprite.addChild text
else
console.warn @lank.thang.id, 'didn\'t know how to draw bounds style:', style
if w > 0 and h > 0 and style is 'border-text'
@sprite.cache -w / 2, -h / 2, w, h, 2
@lastWidth = @lank.thang.width
@lastHeight = @lank.thang.height
buildShadow: ->
shapeName = if @lank.thang.shape in ['ellipsoid', 'disc'] then 'ellipse' else 'rect'
key = "#{shapeName}-shadow"
SHADOW_SIZE = 10
unless key in @layer.spriteSheet.getAnimations()
shape = new createjs.Shape()
shape.graphics.beginFill "rgba(0,0,0)"
bounds = [-SHADOW_SIZE/2, - SHADOW_SIZE/2, SHADOW_SIZE, SHADOW_SIZE]
if shapeName is 'ellipse'
shape.graphics.drawEllipse bounds...
else
shape.graphics.drawRect bounds...
shape.graphics.endFill()
@layer.addCustomGraphic(key, shape, bounds)
alpha = @lank.thang?.alpha ? 1
width = (@lank.thang?.width ? 0) + 0.5
height = (@lank.thang?.height ? 0) + 0.5
longest = Math.max width, height
actualLongest = @lank.thangType.get('shadow') ? longest
width = width * actualLongest / longest
height = height * actualLongest / longest
width *= Camera.PPM
height *= Camera.PPM * @camera.y2x # TODO: doesn't work with rotation
@sprite = new createjs.Sprite(@layer.spriteSheet)
@sprite.gotoAndStop(key)
@sprite.mouseEnabled = false
@sprite.alpha = alpha
@baseScaleX = @sprite.scaleX = width / (@layer.resolutionFactor * SHADOW_SIZE)
@baseScaleY = @sprite.scaleY = height / (@layer.resolutionFactor * SHADOW_SIZE)
buildRadius: (range) ->
alpha = 0.15
colors =
voiceRange: "rgba(0,145,0,#{alpha})"
visualRange: "rgba(0,0,145,#{alpha})"
attackRange: "rgba(145,0,0,#{alpha})"
# Fallback colors which work on both dungeon and grass tiles
extraColors = [
"rgba(145,0,145,#{alpha})"
"rgba(0,145,145,#{alpha})"
"rgba(145,105,0,#{alpha})"
"rgba(225,125,0,#{alpha})"
]
# Find the index of this range, to find the next-smallest radius
rangeNames = @lank.ranges.map((range, index) ->
range['name']
)
i = rangeNames.indexOf(range)
@sprite = new createjs.Shape()
fillColor = colors[range] ? extraColors[i]
@sprite.graphics.beginFill fillColor
# Draw the outer circle
@sprite.graphics.drawCircle 0, 0, @lank.thang[range] * Camera.PPM
# Cut out the hollow part if necessary
if i+1 < @lank.ranges.length
@sprite.graphics.arc 0, 0, @lank.ranges[i+1]['radius'], Math.PI*2, 0, true
@sprite.graphics.endFill()
strokeColor = fillColor.replace '' + alpha, '0.75'
@sprite.graphics.setStrokeStyle 2
@sprite.graphics.beginStroke strokeColor
@sprite.graphics.arc 0, 0, @lank.thang[range] * Camera.PPM, Math.PI*2, 0, true
@sprite.graphics.endStroke()
# Add perspective
@sprite.scaleY *= @camera.y2x
buildDebug: ->
shapeName = if @lank.thang.shape in ['ellipsoid', 'disc'] then 'ellipse' else 'rect'
key = "#{shapeName}-debug"
DEBUG_SIZE = 10
unless key in @layer.spriteSheet.getAnimations()
shape = new createjs.Shape()
shape.graphics.beginFill 'rgba(171,205,239,0.5)'
bounds = [-DEBUG_SIZE / 2, -DEBUG_SIZE / 2, DEBUG_SIZE, DEBUG_SIZE]
if shapeName is 'ellipse'
shape.graphics.drawEllipse bounds...
else
shape.graphics.drawRect bounds...
shape.graphics.endFill()
@layer.addCustomGraphic(key, shape, bounds)
@sprite = new createjs.Sprite(@layer.spriteSheet)
@sprite.gotoAndStop(key)
PX = 3
[w, h] = [Math.max(PX, @lank.thang.width * Camera.PPM), Math.max(PX, @lank.thang.height * Camera.PPM) * @camera.y2x] # TODO: doesn't work with rotation
@sprite.scaleX = w / (@layer.resolutionFactor * DEBUG_SIZE)
@sprite.scaleY = h / (@layer.resolutionFactor * DEBUG_SIZE)
buildSprite: ->
if _.isString @thangType
thangType = markThangTypes[@thangType]
return @loadThangType() if not thangType
@thangType = thangType
return @listenToOnce(@thangType, 'sync', @onLoadedThangType) if not @thangType.loaded
Lank = require './Lank'
# don't bother with making these render async for now, but maybe later for fun and more complexity of code
markLank = new Lank @thangType
markLank.queueAction 'idle'
@sprite = markLank.sprite
@markLank = markLank
@listenTo @markLank, 'new-sprite', (@sprite) ->
loadThangType: ->
name = @thangType
@thangType = new ThangType()
@thangType.url = -> "/db/thang.type/#{name}"
@listenToOnce(@thangType, 'sync', @onLoadedThangType)
@thangType.fetch()
markThangTypes[name] = @thangType
onLoadedThangType: ->
@build()
@toggle(@toggleTo) if @toggleTo?
Backbone.Mediator.publish 'sprite:loaded', {sprite: @}
update: (pos=null) ->
return false unless @on and @sprite
return false if @lank? and not @lank.thangType.isFullyLoaded()
@sprite.visible = not @hidden
@updatePosition pos
@updateRotation()
@updateScale()
if @name is 'highlight' and @highlightDelay and not @highlightTween
@sprite.visible = false
@highlightTween = createjs.Tween.get(@sprite).to({}, @highlightDelay).call =>
@sprite.visible = true
@highlightDelay = @highlightTween = null
@updateAlpha @alpha if @name in ['shadow', 'bounds']
true
updatePosition: (pos) ->
if @lank?.thang and @name in ['shadow', 'debug', 'target', 'selection', 'repair']
pos = @camera.worldToSurface x: @lank.thang.pos.x, y: @lank.thang.pos.y
else
pos ?= @lank?.sprite
return unless pos
@sprite.x = pos.x
@sprite.y = pos.y
if @statusEffect or @name is 'highlight'
offset = @lank.getOffset 'aboveHead'
@sprite.x += offset.x
@sprite.y += offset.y
@sprite.y -= 3 if @statusEffect
updateAlpha: (@alpha) ->
return if not @sprite or @name is 'debug'
if @name is 'shadow'
worldZ = @lank.thang.pos.z - @lank.thang.depth / 2 + @lank.getBobOffset()
@sprite.alpha = @alpha * 0.451 / Math.sqrt(worldZ / 2 + 1)
else if @name is 'bounds'
@drawsBoundsBorderShape?.alpha = Math.floor @lank.thang.alpha # Stop drawing bounds as soon as alpha is reduced at all
else
@sprite.alpha = @alpha
updateRotation: ->
if @name is 'debug' or (@name is 'shadow' and @lank.thang?.shape in ['rectangle', 'box'])
@sprite.rotation = -@lank.thang.rotation * 180 / Math.PI
updateScale: ->
if @name is 'bounds' and ((@lank.thang.width isnt @lastWidth or @lank.thang.height isnt @lastHeight) or (@lank.thang.drawsBoundsIndex isnt @drawsBoundsIndex))
oldMark = @sprite
@buildBounds()
oldMark.parent.addChild @sprite
oldMark.parent.swapChildren oldMark, @sprite
oldMark.parent.removeChild oldMark
if @markLank?
@markLank.scaleFactor = 1.2
@markLank.updateScale()
if @name is 'shadow' and thang = @lank.thang
@sprite.scaleX = @baseScaleX * (thang.scaleFactor ? thang.scaleFactorX ? 1)
@sprite.scaleY = @baseScaleY * (thang.scaleFactor ? thang.scaleFactorY ? 1)
return unless @name in ['selection', 'target', 'repair', 'highlight']
# scale these marks to 10m (100px). Adjust based on lank size.
factor = 0.3 # default size: 3m width, most commonly for target when pointing to a location
if @lank?.sprite
width = @lank.sprite.getBounds()?.width or 0
width /= @lank.options.resolutionFactor
# all targets should be set to have a width of 100px, and then be scaled accordingly
factor = width / 100 # normalize
factor *= 1.1 # add margin
factor = Math.max(factor, 0.3) # lower bound
@sprite.scaleX *= factor
@sprite.scaleY *= factor
if @name in ['selection', 'target', 'repair']
@sprite.scaleY *= @camera.y2x # code applies perspective
stop: -> @markLank?.stop()
play: -> @markLank?.play()
hide: -> @hidden = true
show: -> @hidden = false