mirror of
https://github.com/codeninjasllc/codecombat.git
synced 2024-11-28 18:15:52 -05:00
919e0605e9
Units can be exported as rastered sprite sheets. This is the first part of the project, the second part will be having the game use them.
495 lines
19 KiB
CoffeeScript
495 lines
19 KiB
CoffeeScript
###
|
|
* SpriteStage (WebGL Canvas)
|
|
** Land texture
|
|
** Ground-based selection/target marks, range radii
|
|
** Walls/obstacles
|
|
** Paths and target pieces (and ghosts?)
|
|
** Normal Thangs, bots, wizards (z-indexing based on World-determined sprite.thang.pos.z/y, mainly, instead of sprite-map-determined sprite.z, which we rename to... something)
|
|
** Above-thang marks (blood, highlight) and health bars
|
|
|
|
* Stage (Regular Canvas)
|
|
** Camera border
|
|
** surfaceTextLayer (speech, names)
|
|
** screenLayer
|
|
*** Letterbox
|
|
**** Letterbox top and bottom
|
|
*** FPS display, maybe grid axis labels, coordinate hover
|
|
|
|
** Grid lines--somewhere--we will figure it out, do not really need it at first
|
|
###
|
|
|
|
SpriteBuilder = require 'lib/sprites/SpriteBuilder'
|
|
CocoClass = require 'core/CocoClass'
|
|
SegmentedSprite = require './SegmentedSprite'
|
|
SingularSprite = require './SingularSprite'
|
|
ThangType = require 'models/ThangType'
|
|
|
|
NEVER_RENDER_ANYTHING = false # set to true to test placeholders
|
|
|
|
module.exports = LayerAdapter = class LayerAdapter extends CocoClass
|
|
|
|
# Intermediary between a Surface Stage and a top-level static normal Container or hot-swapped WebGL SpriteContainer.
|
|
# It handles zooming in different ways and, if webGL, creating and assigning spriteSheets.
|
|
|
|
@TRANSFORM_SURFACE: 'surface' # Layer moves/scales/zooms with the Surface of the World
|
|
@TRANSFORM_SURFACE_TEXT: 'surface_text' # Layer moves with the Surface but is size-independent
|
|
@TRANSFORM_SCREEN: 'screen' # Layer stays fixed to the screen
|
|
|
|
# WebGL properties
|
|
actionRenderState: null
|
|
needToRerender: false
|
|
toRenderBundles: null
|
|
willRender: false
|
|
buildAutomatically: true
|
|
buildAsync: true
|
|
resolutionFactor: SPRITE_RESOLUTION_FACTOR
|
|
numThingsLoading: 0
|
|
lanks: null
|
|
spriteSheet: null
|
|
container: null
|
|
customGraphics: null
|
|
|
|
subscriptions:
|
|
'camera:zoom-updated': 'onZoomUpdated'
|
|
|
|
constructor: (options) ->
|
|
super()
|
|
options ?= {}
|
|
@name = options.name ? 'Unnamed'
|
|
@defaultSpriteType = if @name is 'Default' then 'segmented' else 'singular'
|
|
@customGraphics = {}
|
|
@layerPriority = options.layerPriority ? 0
|
|
@transformStyle = options.transform ? LayerAdapter.TRANSFORM_SURFACE
|
|
@camera = options.camera
|
|
@updateLayerOrder = _.throttle @updateLayerOrder, 1000 / 30 # Don't call multiple times in one frame; 30 FPS is probably good enough
|
|
|
|
@webGL = !!options.webGL
|
|
if @webGL
|
|
@initializing = true
|
|
@spriteSheet = @_renderNewSpriteSheet(false) # builds an empty spritesheet
|
|
@container = new createjs.SpriteContainer(@spriteSheet)
|
|
@actionRenderState = {}
|
|
@toRenderBundles = []
|
|
@lanks = []
|
|
@initializing = false
|
|
|
|
else
|
|
@container = new createjs.Container()
|
|
|
|
toString: -> "<Layer #{@layerPriority}: #{@name}>"
|
|
|
|
#- Layer ordering
|
|
|
|
updateLayerOrder: ->
|
|
return if @destroyed
|
|
@container.sortChildren @layerOrderComparator
|
|
|
|
layerOrderComparator: (a, b) ->
|
|
# Optimize
|
|
alp = a.layerPriority or 0
|
|
blp = b.layerPriority or 0
|
|
return alp - blp if alp isnt blp
|
|
# TODO: remove this z stuff
|
|
az = a.z or 1000
|
|
bz = b.z or 1000
|
|
if aLank = a.lank
|
|
if aThang = aLank.thang
|
|
aPos = aThang.pos
|
|
if aThang.health < 0 and aThang.pos.z <= aThang.depth / 2
|
|
# Nice for not being knee deep in the dead, just not nice for ogres flying behind trees when exploded
|
|
--az
|
|
if bLank = b.lank
|
|
if bThang = bLank.thang
|
|
bPos = bThang.pos
|
|
if bThang.health < 0 and bThang.pos.z <= bThang.depth / 2
|
|
--bz
|
|
if az is bz
|
|
return 0 unless aPos and bPos
|
|
return (bPos.y - aPos.y) or (bPos.x - aPos.x)
|
|
return az - bz
|
|
|
|
#- Zoom updating
|
|
|
|
onZoomUpdated: (e) ->
|
|
return unless e.camera is @camera
|
|
if @transformStyle in [LayerAdapter.TRANSFORM_SURFACE, LayerAdapter.TRANSFORM_SURFACE_TEXT]
|
|
change = @container.scaleX / e.zoom
|
|
@container.scaleX = @container.scaleY = e.zoom
|
|
if @webGL
|
|
@container.scaleX *= @camera.canvasScaleFactorX
|
|
@container.scaleY *= @camera.canvasScaleFactorY
|
|
@container.regX = e.surfaceViewport.x
|
|
@container.regY = e.surfaceViewport.y
|
|
if @transformStyle is LayerAdapter.TRANSFORM_SURFACE_TEXT
|
|
for child in @container.children
|
|
continue if child.skipScaling
|
|
child.scaleX *= change
|
|
child.scaleY *= change
|
|
|
|
#- Container-like child functions
|
|
|
|
addChild: (children...) ->
|
|
@container.addChild children...
|
|
if @transformStyle is LayerAdapter.TRANSFORM_SURFACE_TEXT
|
|
for child in children
|
|
continue if child.skipScaling
|
|
child.scaleX /= @container.scaleX
|
|
child.scaleY /= @container.scaleY
|
|
|
|
removeChild: (children...) ->
|
|
@container.removeChild children...
|
|
# TODO: Do we actually need to scale children that were removed?
|
|
if @transformStyle is LayerAdapter.TRANSFORM_SURFACE_TEXT
|
|
for child in children
|
|
child.scaleX *= @container.scaleX
|
|
child.scaleY *= @container.scaleY
|
|
|
|
#- Adding, removing children for WebGL layers.
|
|
|
|
addLank: (lank) ->
|
|
lank.options.resolutionFactor = @resolutionFactor
|
|
|
|
lank.layer = @
|
|
@listenTo(lank, 'action-needs-render', @onActionNeedsRender)
|
|
@lanks.push lank
|
|
@loadThangType(lank.thangType)
|
|
@addDefaultActionsToRender(lank)
|
|
@setSpriteToLank(lank)
|
|
@updateLayerOrder()
|
|
lank.addHealthBar()
|
|
|
|
removeLank: (lank) ->
|
|
@stopListening(lank)
|
|
lank.layer = null
|
|
@container.removeChild lank.sprite
|
|
@lanks = _.without @lanks, lank
|
|
|
|
#- Loading network resources dynamically
|
|
|
|
loadThangType: (thangType) ->
|
|
if not thangType.isFullyLoaded()
|
|
thangType.setProjection null
|
|
thangType.fetch() unless thangType.loading
|
|
@numThingsLoading++
|
|
@listenToOnce(thangType, 'sync', @somethingLoaded)
|
|
else if thangType.get('raster') and not thangType.loadedRaster
|
|
thangType.loadRasterImage()
|
|
@listenToOnce(thangType, 'raster-image-loaded', @somethingLoaded)
|
|
@numThingsLoading++
|
|
|
|
somethingLoaded: (thangType) ->
|
|
@numThingsLoading--
|
|
@loadThangType(thangType) # might need to load the raster image object
|
|
for lank in @lanks
|
|
if lank.thangType is thangType
|
|
@addDefaultActionsToRender(lank)
|
|
@renderNewSpriteSheet()
|
|
|
|
#- Adding to the list of things we need to render
|
|
|
|
onActionNeedsRender: (lank, action) ->
|
|
@upsertActionToRender(lank.thangType, action.name, lank.options.colorConfig)
|
|
|
|
addDefaultActionsToRender: (lank) ->
|
|
needToRender = false
|
|
if lank.thangType.get('raster')
|
|
@upsertActionToRender(lank.thangType)
|
|
else
|
|
for action in _.values(lank.thangType.getActions())
|
|
continue unless _.any ThangType.defaultActions, (prefix) -> _.string.startsWith(action.name, prefix)
|
|
@upsertActionToRender(lank.thangType, action.name, lank.options.colorConfig)
|
|
|
|
upsertActionToRender: (thangType, actionName, colorConfig) ->
|
|
groupKey = @renderGroupingKey(thangType, actionName, colorConfig)
|
|
return false if @actionRenderState[groupKey] isnt undefined
|
|
@actionRenderState[groupKey] = 'need-to-render'
|
|
@toRenderBundles.push({thangType: thangType, actionName: actionName, colorConfig: colorConfig})
|
|
return true if @willRender or not @buildAutomatically
|
|
@willRender = _.defer => @renderNewSpriteSheet()
|
|
return true
|
|
|
|
addCustomGraphic: (key, graphic, bounds) ->
|
|
return false if @customGraphics[key]
|
|
@customGraphics[key] = { graphic: graphic, bounds: new createjs.Rectangle(bounds...) }
|
|
return true if @willRender or not @buildAutomatically
|
|
@_renderNewSpriteSheet(false)
|
|
|
|
#- Rendering sprite sheets
|
|
|
|
renderNewSpriteSheet: ->
|
|
@willRender = false
|
|
return if @numThingsLoading
|
|
@_renderNewSpriteSheet()
|
|
|
|
_renderNewSpriteSheet: (async) ->
|
|
@asyncBuilder.stopAsync() if @asyncBuilder
|
|
@asyncBuilder = null
|
|
|
|
async ?= @buildAsync
|
|
builder = new createjs.SpriteSheetBuilder()
|
|
groups = _.groupBy(@toRenderBundles, ((bundle) -> @renderGroupingKey(bundle.thangType, '', bundle.colorConfig)), @)
|
|
|
|
# The first frame is always the 'loading', ie placeholder, image.
|
|
placeholder = @createPlaceholder()
|
|
dimension = @resolutionFactor * SPRITE_PLACEHOLDER_WIDTH
|
|
placeholder.setBounds(0, 0, dimension, dimension)
|
|
builder.addFrame(placeholder)
|
|
|
|
# Add custom graphics
|
|
extantGraphics = if @spriteSheet?.resolutionFactor is @resolutionFactor then @spriteSheet.getAnimations() else []
|
|
for key, graphic of @customGraphics
|
|
if key in extantGraphics
|
|
graphic = new createjs.Sprite(@spriteSheet)
|
|
graphic.gotoAndStop(key)
|
|
frame = builder.addFrame(graphic)
|
|
else
|
|
frame = builder.addFrame(graphic.graphic, graphic.bounds, @resolutionFactor)
|
|
builder.addAnimation(key, [frame], false)
|
|
|
|
# Render ThangTypes
|
|
groups = {} if NEVER_RENDER_ANYTHING
|
|
for bundleGrouping in _.values(groups)
|
|
thangType = bundleGrouping[0].thangType
|
|
colorConfig = bundleGrouping[0].colorConfig
|
|
actionNames = (bundle.actionName for bundle in bundleGrouping)
|
|
args = [thangType, colorConfig, actionNames, builder]
|
|
if thangType.get('raw')
|
|
if (thangType.get('spriteType') or @defaultSpriteType) is 'segmented'
|
|
@renderSegmentedThangType(args...)
|
|
else
|
|
@renderSingularThangType(args...)
|
|
else
|
|
@renderRasterThangType(thangType, builder)
|
|
|
|
if async
|
|
try
|
|
builder.buildAsync()
|
|
catch e
|
|
@resolutionFactor *= 0.9
|
|
return @_renderNewSpriteSheet(async)
|
|
builder.on 'complete', @onBuildSpriteSheetComplete, @, true, builder
|
|
@asyncBuilder = builder
|
|
else
|
|
sheet = builder.build()
|
|
@onBuildSpriteSheetComplete({async:async}, builder)
|
|
return sheet
|
|
|
|
onBuildSpriteSheetComplete: (e, builder) ->
|
|
return if @initializing or @destroyed
|
|
@asyncBuilder = null
|
|
|
|
if builder.spriteSheet._images.length > 1
|
|
total = 0
|
|
# get a rough estimate of how much smaller the spritesheet needs to be
|
|
for image, index in builder.spriteSheet._images
|
|
total += image.height / builder.maxHeight
|
|
@resolutionFactor /= (Math.max(1.1, Math.sqrt(total)))
|
|
@_renderNewSpriteSheet(e.async)
|
|
return
|
|
|
|
@spriteSheet = builder.spriteSheet
|
|
@spriteSheet.resolutionFactor = @resolutionFactor
|
|
oldLayer = @container
|
|
@container = new createjs.SpriteContainer(@spriteSheet)
|
|
for lank in @lanks
|
|
console.log 'zombie sprite found on layer', @name if lank.destroyed
|
|
continue if lank.destroyed
|
|
@setSpriteToLank(lank)
|
|
for prop in ['scaleX', 'scaleY', 'regX', 'regY']
|
|
@container[prop] = oldLayer[prop]
|
|
if parent = oldLayer.parent
|
|
index = parent.getChildIndex(oldLayer)
|
|
parent.removeChildAt(index)
|
|
parent.addChildAt(@container, index)
|
|
@camera?.updateZoom(true)
|
|
@updateLayerOrder()
|
|
for lank in @lanks
|
|
lank.options.resolutionFactor = @resolutionFactor
|
|
lank.updateScale()
|
|
lank.updateRotation()
|
|
@trigger 'new-spritesheet'
|
|
|
|
resetSpriteSheet: ->
|
|
@removeLank(lank) for lank in @lanks.slice(0)
|
|
@toRenderBundles = []
|
|
@actionRenderState = {}
|
|
@initializing = true
|
|
@spriteSheet = @_renderNewSpriteSheet(false) # builds an empty spritesheet
|
|
@initializing = false
|
|
|
|
#- Placeholder
|
|
|
|
createPlaceholder: ->
|
|
# TODO: Experiment with this. Perhaps have rectangles if default layer is obstacle or floor,
|
|
# and different colors for different layers.
|
|
g = new createjs.Graphics()
|
|
g.setStrokeStyle(5)
|
|
color = {
|
|
'Land': [0, 50, 0]
|
|
'Ground': [230, 230, 230]
|
|
'Obstacle': [20, 70, 20]
|
|
'Path': [200, 100, 200]
|
|
'Default': [64, 64, 64]
|
|
'Floating': [100, 100, 200]
|
|
}[@name] or [0, 0, 0]
|
|
g.beginStroke(createjs.Graphics.getRGB(color...))
|
|
color.push 0.7
|
|
g.beginFill(createjs.Graphics.getRGB(color...))
|
|
width = @resolutionFactor * SPRITE_PLACEHOLDER_WIDTH
|
|
bounds = [0, 0, width, width]
|
|
if @name in ['Default', 'Ground', 'Floating', 'Path']
|
|
g.drawEllipse(bounds...)
|
|
else
|
|
g.drawRect(bounds...)
|
|
new createjs.Shape(g)
|
|
|
|
#- Rendering containers for segmented thang types
|
|
|
|
renderSegmentedThangType: (thangType, colorConfig, actionNames, spriteSheetBuilder) ->
|
|
containersToRender = thangType.getContainersForActions(actionNames)
|
|
spriteBuilder = new SpriteBuilder(thangType, {colorConfig: colorConfig})
|
|
for containerGlobalName in containersToRender
|
|
containerKey = @renderGroupingKey(thangType, containerGlobalName, colorConfig)
|
|
if @spriteSheet?.resolutionFactor is @resolutionFactor and containerKey in @spriteSheet.getAnimations()
|
|
container = new createjs.Sprite(@spriteSheet)
|
|
container.gotoAndStop(containerKey)
|
|
frame = spriteSheetBuilder.addFrame(container)
|
|
else
|
|
container = spriteBuilder.buildContainerFromStore(containerGlobalName)
|
|
frame = spriteSheetBuilder.addFrame(container, null, @resolutionFactor * (thangType.get('scale') or 1))
|
|
spriteSheetBuilder.addAnimation(containerKey, [frame], false)
|
|
|
|
#- Rendering sprite sheets for singular thang types
|
|
|
|
renderSingularThangType: (thangType, colorConfig, actionNames, spriteSheetBuilder) ->
|
|
actionObjects = _.values(thangType.getActions())
|
|
animationActions = []
|
|
for a in actionObjects
|
|
continue unless a.animation
|
|
continue unless a.name in actionNames
|
|
animationActions.push(a)
|
|
|
|
spriteBuilder = new SpriteBuilder(thangType, {colorConfig: colorConfig})
|
|
|
|
animationGroups = _.groupBy animationActions, (action) -> action.animation
|
|
for animationName, actions of animationGroups
|
|
renderAll = _.any actions, (action) -> action.frames is undefined
|
|
scale = actions[0].scale or thangType.get('scale') or 1
|
|
|
|
actionKeys = (@renderGroupingKey(thangType, action.name, colorConfig) for action in actions)
|
|
if @spriteSheet?.resolutionFactor is @resolutionFactor and _.all(actionKeys, (key) => key in @spriteSheet.getAnimations())
|
|
framesNeeded = _.uniq(_.flatten((@spriteSheet.getAnimation(key)).frames for key in actionKeys))
|
|
framesMap = {}
|
|
for frame in framesNeeded
|
|
sprite = new createjs.Sprite(@spriteSheet)
|
|
sprite.gotoAndStop(frame)
|
|
framesMap[frame] = spriteSheetBuilder.addFrame(sprite)
|
|
for key, index in actionKeys
|
|
action = actions[index]
|
|
frames = (framesMap[f] for f in @spriteSheet.getAnimation(key).frames)
|
|
next = @nextForAction(action)
|
|
spriteSheetBuilder.addAnimation(key, frames, next)
|
|
continue
|
|
|
|
mc = spriteBuilder.buildMovieClip(animationName, null, null, null, {'temp':0})
|
|
|
|
if renderAll
|
|
res = spriteSheetBuilder.addMovieClip(mc, null, scale * @resolutionFactor)
|
|
frames = spriteSheetBuilder._animations['temp'].frames
|
|
framesMap = _.zipObject _.range(frames.length), frames
|
|
else
|
|
framesMap = {}
|
|
framesToRender = _.uniq(_.flatten((a.frames.split(',') for a in actions)))
|
|
for frame in framesToRender
|
|
frame = parseInt(frame)
|
|
f = _.bind(mc.gotoAndStop, mc, frame)
|
|
framesMap[frame] = spriteSheetBuilder.addFrame(mc, null, scale * @resolutionFactor, f)
|
|
|
|
for action in actions
|
|
name = @renderGroupingKey(thangType, action.name, colorConfig)
|
|
|
|
if action.frames
|
|
frames = (framesMap[parseInt(frame)] for frame in action.frames.split(','))
|
|
else
|
|
frames = _.sortBy(_.values(framesMap))
|
|
next = @nextForAction(action)
|
|
spriteSheetBuilder.addAnimation(name, frames, next)
|
|
|
|
containerActions = []
|
|
for a in actionObjects
|
|
continue unless a.container
|
|
continue unless a.name in actionNames
|
|
containerActions.push(a)
|
|
|
|
containerGroups = _.groupBy containerActions, (action) -> action.container
|
|
for containerName, actions of containerGroups
|
|
container = spriteBuilder.buildContainerFromStore(containerName)
|
|
scale = actions[0].scale or thangType.get('scale') or 1
|
|
frame = spriteSheetBuilder.addFrame(container, null, scale * @resolutionFactor)
|
|
for action in actions
|
|
name = @renderGroupingKey(thangType, action.name, colorConfig)
|
|
spriteSheetBuilder.addAnimation(name, [frame], false)
|
|
|
|
nextForAction: (action) ->
|
|
next = true
|
|
next = action.goesTo if action.goesTo
|
|
next = false if action.loops is false
|
|
return next
|
|
|
|
#- Rendering frames for raster thang types
|
|
|
|
renderRasterThangType: (thangType, spriteSheetBuilder) ->
|
|
unless thangType.rasterImage
|
|
console.error("Cannot render the LayerAdapter SpriteSheet until the raster image for <#{thangType.get('name')}> is loaded.")
|
|
|
|
bm = new createjs.Bitmap(thangType.rasterImage[0])
|
|
scale = thangType.get('scale') or 1
|
|
frame = spriteSheetBuilder.addFrame(bm, null, scale)
|
|
spriteSheetBuilder.addAnimation(@renderGroupingKey(thangType), [frame], false)
|
|
|
|
#- Distributing new Segmented/Singular/RasterSprites to Lanks
|
|
|
|
setSpriteToLank: (lank) ->
|
|
if not lank.thangType.isFullyLoaded()
|
|
# just give a placeholder
|
|
sprite = new createjs.Sprite(@spriteSheet)
|
|
sprite.gotoAndStop(0)
|
|
sprite.placeholder = true
|
|
sprite.regX = @resolutionFactor * SPRITE_PLACEHOLDER_WIDTH / 2
|
|
sprite.regY = @resolutionFactor * SPRITE_PLACEHOLDER_WIDTH
|
|
sprite.baseScaleX = sprite.baseScaleY = sprite.scaleX = sprite.scaleY = 10 / (@resolutionFactor * SPRITE_PLACEHOLDER_WIDTH)
|
|
|
|
else if lank.thangType.get('raster')
|
|
sprite = new createjs.Sprite(@spriteSheet)
|
|
scale = lank.thangType.get('scale') or 1
|
|
reg = lank.getOffset 'registration'
|
|
sprite.regX = -reg.x * scale
|
|
sprite.regY = -reg.y * scale
|
|
sprite.gotoAndStop(@renderGroupingKey(lank.thangType))
|
|
sprite.baseScaleX = sprite.baseScaleY = 1
|
|
|
|
else
|
|
SpriteClass = if (lank.thangType.get('spriteType') or @defaultSpriteType) is 'segmented' then SegmentedSprite else SingularSprite
|
|
prefix = @renderGroupingKey(lank.thangType, null, lank.options.colorConfig) + '.'
|
|
sprite = new SpriteClass(@spriteSheet, lank.thangType, prefix, @resolutionFactor)
|
|
|
|
sprite.lank = lank
|
|
sprite.camera = @camera
|
|
sprite.layerPriority = lank.thang?.layerPriority ? lank.thangType.get 'layerPriority'
|
|
sprite.name = lank.thang?.spriteName or lank.thangType.get 'name'
|
|
lank.setSprite(sprite)
|
|
lank.update(true)
|
|
@container.addChild(sprite)
|
|
lank.updateScale true if lank.thangType.get 'matchWorldDimensions' # Otherwise it's at the wrong scale for some reason.
|
|
|
|
renderGroupingKey: (thangType, grouping, colorConfig) ->
|
|
key = thangType.get('slug')
|
|
for colorKey, colorValue of colorConfig ? {}
|
|
key += "(#{colorKey}:#{colorValue.hue},#{colorValue.saturation},#{colorValue.lightness})"
|
|
key += '.'+grouping if grouping
|
|
key
|
|
|
|
destroy: ->
|
|
child.destroy?() for child in @container.children
|
|
@asyncBuilder.stopAsync() if @asyncBuilder
|
|
super()
|