codecombat/app/lib/surface/Mark.coffee

345 lines
12 KiB
CoffeeScript
Raw Normal View History

2014-01-03 13:32:13 -05:00
CocoClass = require 'lib/CocoClass'
Camera = require './Camera'
ThangType = require 'models/ThangType'
markThangTypes = {}
2014-01-03 13:32:13 -05:00
module.exports = class Mark extends CocoClass
subscriptions: {}
2014-06-22 01:31:10 -04:00
alpha: 1
2014-01-03 13:32:13 -05:00
constructor: (options) ->
super()
options ?= {}
@name = options.name
@sprite = options.sprite
@camera = options.camera
@layer = options.layer
@thangType = options.thangType
2014-09-24 20:52:44 -04:00
@listenTo @layer, 'new-spritesheet', @onLayerMadeSpriteSheet
2014-06-30 22:16:26 -04:00
console.error @toString(), 'needs a name.' unless @name
console.error @toString(), 'needs a camera.' unless @camera
console.error @toString(), 'needs a layer.' unless @layer
2014-01-03 13:32:13 -05:00
@build()
destroy: ->
createjs.Tween.removeTweens @mark if @mark
2014-01-03 13:32:13 -05:00
@mark?.parent?.removeChild @mark
if @markSprite
@layer.removeCocoSprite(@markSprite)
@markSprite.destroy()
2014-02-11 18:38:36 -05:00
@sprite = null
super()
2014-01-03 13:32:13 -05:00
toString: -> "<Mark #{@name}: Sprite #{@sprite?.thang?.id ? 'None'}>"
2014-09-24 20:52:44 -04:00
onLayerMadeSpriteSheet: ->
return unless @mark
2014-09-24 21:42:04 -04:00
return @update() if @markSprite
# need to update the mark display object manually...
2014-09-24 20:52:44 -04:00
@mark = null
@build()
@layer.addChild @mark
@layer.updateLayerOrder()
2014-01-03 13:32:13 -05:00
toggle: (to) ->
to = !!to
2014-01-03 13:32:13 -05:00
return @ if to is @on
return @toggleTo = to unless @mark
2014-01-03 13:32:13 -05:00
@on = to
delete @toggleTo
2014-01-03 13:32:13 -05:00
if @on
2014-09-24 21:42:04 -04:00
if @markSprite
@layer.addCocoSprite(@markSprite)
else
@layer.addChild @mark
@layer.updateLayerOrder()
2014-01-03 13:32:13 -05:00
else
2014-09-24 21:42:04 -04:00
if @markSprite
@layer.removeCocoSprite(@markSprite)
else
@layer.removeChild @mark
2014-01-03 13:32:13 -05:00
if @highlightTween
@highlightDelay = @highlightTween = null
createjs.Tween.removeTweens @mark
@mark.visible = true
@
setLayer: (layer) ->
return if layer is @layer
wasOn = @on
@toggle false
@layer = layer
@toggle true if wasOn
2014-01-03 13:32:13 -05:00
setSprite: (sprite) ->
return if sprite is @sprite
@sprite = sprite
@build()
@
build: ->
unless @mark
if @name is 'bounds' then @buildBounds()
else if @name is 'shadow' then @buildShadow()
2014-01-03 13:32:13 -05:00
else if @name is 'debug' then @buildDebug()
2014-04-15 11:39:18 -04:00
else if @name.match(/.+(Range|Distance|Radius)$/) then @buildRadius(@name)
2014-01-03 13:32:13 -05:00
else if @thangType then @buildSprite()
2014-06-30 22:16:26 -04:00
else console.error 'Don\'t know how to build mark for', @name
@mark?.mouseEnabled = false
2014-01-03 13:32:13 -05:00
@
buildBounds: ->
@mark = new createjs.Container()
@mark.mouseChildren = false
style = @sprite.thang.drawsBoundsStyle
2014-07-14 20:54:27 -04:00
@drawsBoundsIndex = @sprite.thang.drawsBoundsIndex
return if style is 'corner-text' and @sprite.thang.world.age is 0
2014-01-03 13:32:13 -05:00
# Confusingly make some semi-random colors that'll be consistent based on the drawsBoundsIndex
2014-06-24 18:45:27 -04:00
colors = (128 + Math.floor(('0.'+Math.sin(3 * @drawsBoundsIndex + i).toString().substr(6)) * 128) for i in [1 ... 4])
2014-01-03 13:32:13 -05:00
color = "rgba(#{colors[0]}, #{colors[1]}, #{colors[2]}, 0.5)"
[w, h] = [@sprite.thang.width * Camera.PPM, @sprite.thang.height * Camera.PPM * @camera.y2x]
if style in ['border-text', 'corner-text']
@drawsBoundsBorderShape = shape = new createjs.Shape()
2014-06-24 18:45:27 -04:00
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
2014-06-30 22:16:26 -04:00
if @sprite.thang.shape in ['ellipsoid', 'disc']
2014-06-24 18:45:27 -04:00
shape.drawEllipse 0, 0, w, h
else
shape.graphics.drawRect -w / 2, -h / 2, w, h
shape.graphics.endStroke()
shape.graphics.endFill()
@mark.addChild shape
if style is 'border-text'
2014-06-30 22:16:26 -04:00
text = new createjs.Text '' + @drawsBoundsIndex, '20px Arial', color.replace('0.5', '1')
2014-06-24 18:45:27 -04:00
text.regX = text.getMeasuredWidth() / 2
text.regY = text.getMeasuredHeight() / 2
2014-06-30 22:16:26 -04:00
text.shadow = new createjs.Shadow('#000000', 1, 1, 0)
2014-06-24 18:45:27 -04:00
@mark.addChild text
else if style is 'corner-text'
2014-06-24 18:45:27 -04:00
return if @sprite.thang.world.age is 0
2014-06-30 22:16:26 -04:00
letter = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ'[@drawsBoundsIndex % 26]
text = new createjs.Text letter, '14px Arial', '#333333' # color.replace('0.5', '1')
2014-06-24 18:45:27 -04:00
text.x = -w / 2 + 2
text.y = -h / 2 + 2
@mark.addChild text
else
console.warn @sprite.thang.id, 'didn\'t know how to draw bounds style:', style
2014-01-03 13:32:13 -05:00
if w > 0 and h > 0 and style is 'border-text'
2014-01-03 13:32:13 -05:00
@mark.cache -w / 2, -h / 2, w, h, 2
@lastWidth = @sprite.thang.width
@lastHeight = @sprite.thang.height
buildShadow: ->
2014-09-24 20:52:44 -04:00
shapeName = if @sprite.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 = @sprite.thang?.alpha ? 1
width = (@sprite.thang?.width ? 0) + 0.5
height = (@sprite.thang?.height ? 0) + 0.5
longest = Math.max width, height
actualLongest = @sprite.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
2014-09-24 20:52:44 -04:00
@mark = new createjs.Sprite(@layer.spriteSheet)
@mark.gotoAndStop(key)
2014-01-03 13:32:13 -05:00
@mark.mouseEnabled = false
2014-09-24 20:52:44 -04:00
@mark.alpha = alpha
@baseScaleX = @mark.scaleX = width / (@layer.resolutionFactor * SHADOW_SIZE)
@baseScaleY = @mark.scaleY = height / (@layer.resolutionFactor * SHADOW_SIZE)
2014-01-03 13:32:13 -05:00
buildRadius: (range) ->
alpha = 0.15
colors =
2014-06-30 22:16:26 -04:00
voiceRange: "rgba(0,145,0,#{alpha})"
visualRange: "rgba(0,0,145,#{alpha})"
attackRange: "rgba(145,0,0,#{alpha})"
2014-03-18 08:43:38 -04:00
# Fallback colors which work on both dungeon and grass tiles
extraColors = [
2014-06-30 22:16:26 -04:00
"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 = @sprite.ranges.map((range, index) ->
range['name']
)
i = rangeNames.indexOf(range)
@mark = new createjs.Shape()
fillColor = colors[range] ? extraColors[i]
@mark.graphics.beginFill fillColor
2014-03-20 13:43:24 -04:00
# Draw the outer circle
@mark.graphics.drawCircle 0, 0, @sprite.thang[range] * Camera.PPM
# Cut out the hollow part if necessary
if i+1 < @sprite.ranges.length
@mark.graphics.arc 0, 0, @sprite.ranges[i+1]['radius'], Math.PI*2, 0, true
@mark.graphics.endFill()
strokeColor = fillColor.replace '' + alpha, '0.75'
@mark.graphics.setStrokeStyle 2
@mark.graphics.beginStroke strokeColor
@mark.graphics.arc 0, 0, @sprite.thang[range] * Camera.PPM, Math.PI*2, 0, true
@mark.graphics.endStroke()
# Add perspective
@mark.scaleY *= @camera.y2x
2014-01-03 13:32:13 -05:00
buildDebug: ->
2014-09-26 17:09:44 -04:00
shapeName = if @sprite.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)
@mark = new createjs.Sprite(@layer.spriteSheet)
@mark.gotoAndStop(key)
2014-01-03 13:32:13 -05:00
PX = 3
2014-07-16 18:57:53 -04:00
[w, h] = [Math.max(PX, @sprite.thang.width * Camera.PPM), Math.max(PX, @sprite.thang.height * Camera.PPM) * @camera.y2x] # TODO: doesn't work with rotation
2014-09-26 17:09:44 -04:00
@mark.scaleX = w / (@layer.resolutionFactor * DEBUG_SIZE)
@mark.scaleY = h / (@layer.resolutionFactor * DEBUG_SIZE)
2014-01-03 13:32:13 -05:00
buildSprite: ->
if _.isString @thangType
thangType = markThangTypes[@thangType]
return @loadThangType() if not thangType
@thangType = thangType
2014-03-24 12:58:34 -04:00
return @listenToOnce(@thangType, 'sync', @onLoadedThangType) if not @thangType.loaded
2014-01-03 13:32:13 -05:00
CocoSprite = require './CocoSprite'
2014-05-20 13:46:52 -04:00
# don't bother with making these render async for now, but maybe later for fun and more complexity of code
2014-09-24 21:42:04 -04:00
markSprite = new CocoSprite @thangType
markSprite.queueAction 'idle'
@mark = markSprite.imageObject
2014-02-11 18:38:36 -05:00
@markSprite = markSprite
2014-09-24 21:42:04 -04:00
@listenTo @markSprite, 'new-image-object', (@mark) ->
2014-01-03 13:32:13 -05:00
loadThangType: ->
name = @thangType
@thangType = new ThangType()
@thangType.url = -> "/db/thang.type/#{name}"
2014-03-24 12:58:34 -04:00
@listenToOnce(@thangType, 'sync', @onLoadedThangType)
@thangType.fetch()
markThangTypes[name] = @thangType
onLoadedThangType: ->
@build()
@toggle(@toggleTo) if @toggleTo?
Backbone.Mediator.publish 'sprite:loaded', {sprite: @}
2014-01-03 13:32:13 -05:00
update: (pos=null) ->
2014-05-19 19:17:54 -04:00
return false unless @on and @mark
return false if @sprite? and not @sprite.thangType.isFullyLoaded()
@mark.visible = not @hidden
2014-01-03 13:32:13 -05:00
@updatePosition pos
@updateRotation()
@updateScale()
if @name is 'highlight' and @highlightDelay and not @highlightTween
@mark.visible = false
@highlightTween = createjs.Tween.get(@mark).to({}, @highlightDelay).call =>
@mark.visible = true
@highlightDelay = @highlightTween = null
@updateAlpha @alpha if @name in ['shadow', 'bounds']
2014-01-03 13:32:13 -05:00
true
updatePosition: (pos) ->
2014-06-22 01:31:10 -04:00
if @sprite?.thang and @name in ['shadow', 'debug', 'target', 'selection', 'repair']
2014-01-03 13:32:13 -05:00
pos = @camera.worldToSurface x: @sprite.thang.pos.x, y: @sprite.thang.pos.y
else
pos ?= @sprite?.imageObject
return unless pos
2014-01-03 13:32:13 -05:00
@mark.x = pos.x
@mark.y = pos.y
if @statusEffect or @name is 'highlight'
2014-01-03 13:32:13 -05:00
offset = @sprite.getOffset 'aboveHead'
@mark.x += offset.x
@mark.y += offset.y
@mark.y -= 3 if @statusEffect
2014-01-03 13:32:13 -05:00
2014-06-22 01:31:10 -04:00
updateAlpha: (@alpha) ->
return if not @mark or @name is 'debug'
if @name is 'shadow'
worldZ = @sprite.thang.pos.z - @sprite.thang.depth / 2 + @sprite.getBobOffset()
@mark.alpha = @alpha * 0.451 / Math.sqrt(worldZ / 2 + 1)
else if @name is 'bounds'
@drawsBoundsBorderShape?.alpha = Math.floor @sprite.thang.alpha # Stop drawing bounds as soon as alpha is reduced at all
else
2014-06-22 01:31:10 -04:00
@mark.alpha = @alpha
2014-01-03 13:32:13 -05:00
updateRotation: ->
2014-06-30 22:16:26 -04:00
if @name is 'debug' or (@name is 'shadow' and @sprite.thang?.shape in ['rectangle', 'box'])
2014-07-16 18:57:53 -04:00
@mark.rotation = -@sprite.thang.rotation * 180 / Math.PI
2014-01-03 13:32:13 -05:00
updateScale: ->
2014-06-24 18:45:27 -04:00
if @name is 'bounds' and ((@sprite.thang.width isnt @lastWidth or @sprite.thang.height isnt @lastHeight) or (@sprite.thang.drawsBoundsIndex isnt @drawsBoundsIndex))
2014-01-03 13:32:13 -05:00
oldMark = @mark
@buildBounds()
oldMark.parent.addChild @mark
oldMark.parent.swapChildren oldMark, @mark
oldMark.parent.removeChild oldMark
2014-06-22 01:31:10 -04:00
2014-05-20 01:17:01 -04:00
if @markSprite?
2014-05-20 02:01:48 -04:00
@markSprite.scaleFactor = 1.2
2014-05-20 01:17:01 -04:00
@markSprite.updateScale()
2014-06-24 18:45:27 -04:00
if @name is 'shadow' and thang = @sprite.thang
2014-09-24 20:52:44 -04:00
@mark.scaleX = @baseScaleX * (thang.scaleFactor ? thang.scaleFactorX ? 1)
@mark.scaleY = @baseScaleY * (thang.scaleFactor ? thang.scaleFactorY ? 1)
2014-06-24 18:45:27 -04:00
2014-06-30 22:16:26 -04:00
return unless @name in ['selection', 'target', 'repair', 'highlight']
2014-06-22 01:31:10 -04:00
# scale these marks to 10m (100px). Adjust based on sprite size.
factor = 0.3 # default size: 3m width, most commonly for target when pointing to a location
if @sprite?.imageObject
width = @sprite.imageObject.getBounds()?.width or 0
width /= @sprite.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
@mark.scaleX *= factor
@mark.scaleY *= factor
2014-06-22 01:31:10 -04:00
2014-01-03 13:32:13 -05:00
if @name in ['selection', 'target', 'repair']
@mark.scaleY *= @camera.y2x # code applies perspective
stop: -> @markSprite?.stop()
play: -> @markSprite?.play()
hide: -> @hidden = true
show: -> @hidden = false