mirror of
https://github.com/codeninjasllc/codecombat.git
synced 2025-03-14 07:00:01 -04:00
Add spriteSheets to ThangType, export modal to Thang Editor
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.
This commit is contained in:
parent
b38a6c5060
commit
919e0605e9
9 changed files with 307 additions and 34 deletions
98
app/lib/sprites/SpriteExporter.coffee
Normal file
98
app/lib/sprites/SpriteExporter.coffee
Normal file
|
@ -0,0 +1,98 @@
|
|||
SpriteBuilder = require('./SpriteBuilder')
|
||||
ThangType = require('models/ThangType')
|
||||
CocoClass = require('core/CocoClass')
|
||||
|
||||
|
||||
class SpriteExporter extends CocoClass
|
||||
'''
|
||||
To be used by the ThangTypeEditView to export ThangTypes to single sprite sheets which can be uploaded to
|
||||
GridFS and used in gameplay, avoiding rendering vector images.
|
||||
|
||||
Code has been copied and reworked and simplified from LayerAdapter. Some shared code has been refactored into
|
||||
ThangType, but more work could be done to rethink and reorganize Sprite rendering.
|
||||
'''
|
||||
|
||||
constructor: (thangType, options) ->
|
||||
@thangType = thangType
|
||||
options ?= {}
|
||||
@colorConfig = options.colorConfig or {}
|
||||
@resolutionFactor = options.resolutionFactor or 1
|
||||
@actionNames = options.actionNames or (action.name for action in @thangType.getDefaultActions())
|
||||
super()
|
||||
|
||||
build: (renderType) ->
|
||||
spriteSheetBuilder = new createjs.SpriteSheetBuilder()
|
||||
if (renderType or @thangType.get('spriteType') or 'segmented') is 'segmented'
|
||||
@renderSegmentedThangType(spriteSheetBuilder)
|
||||
else
|
||||
@renderSingularThangType(spriteSheetBuilder)
|
||||
try
|
||||
spriteSheetBuilder.buildAsync()
|
||||
catch e
|
||||
@resolutionFactor *= 0.9
|
||||
return @build()
|
||||
spriteSheetBuilder.on 'complete', @onBuildSpriteSheetComplete, @, true, spriteSheetBuilder
|
||||
@asyncBuilder = spriteSheetBuilder
|
||||
|
||||
renderSegmentedThangType: (spriteSheetBuilder) ->
|
||||
containersToRender = @thangType.getContainersForActions(@actionNames)
|
||||
spriteBuilder = new SpriteBuilder(@thangType, {colorConfig: @colorConfig})
|
||||
for containerGlobalName in containersToRender
|
||||
container = spriteBuilder.buildContainerFromStore(containerGlobalName)
|
||||
frame = spriteSheetBuilder.addFrame(container, null, @resolutionFactor * (@thangType.get('scale') or 1))
|
||||
spriteSheetBuilder.addAnimation(containerGlobalName, [frame], false)
|
||||
|
||||
renderSingularThangType: (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
|
||||
scale = actions[0].scale or @thangType.get('scale') or 1
|
||||
mc = spriteBuilder.buildMovieClip(animationName, null, null, null, {'temp':0})
|
||||
spriteSheetBuilder.addMovieClip(mc, null, scale * @resolutionFactor)
|
||||
frames = spriteSheetBuilder._animations['temp'].frames
|
||||
framesMap = _.zipObject _.range(frames.length), frames
|
||||
for action in actions
|
||||
if action.frames
|
||||
frames = (framesMap[parseInt(frame)] for frame in action.frames.split(','))
|
||||
else
|
||||
frames = _.sortBy(_.values(framesMap))
|
||||
next = @nextForAction(action)
|
||||
spriteSheetBuilder.addAnimation(action.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
|
||||
spriteSheetBuilder.addAnimation(action.name, [frame], false)
|
||||
|
||||
onBuildSpriteSheetComplete: (e, builder) ->
|
||||
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
|
||||
|
||||
@trigger 'build', { spriteSheet: builder.spriteSheet }
|
||||
|
||||
|
||||
|
||||
module.exports = SpriteExporter
|
|
@ -22,6 +22,7 @@ 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
|
||||
|
||||
|
@ -42,7 +43,6 @@ module.exports = LayerAdapter = class LayerAdapter extends CocoClass
|
|||
buildAutomatically: true
|
||||
buildAsync: true
|
||||
resolutionFactor: SPRITE_RESOLUTION_FACTOR
|
||||
defaultActions: ['idle', 'die', 'move', 'attack']
|
||||
numThingsLoading: 0
|
||||
lanks: null
|
||||
spriteSheet: null
|
||||
|
@ -196,7 +196,7 @@ module.exports = LayerAdapter = class LayerAdapter extends CocoClass
|
|||
@upsertActionToRender(lank.thangType)
|
||||
else
|
||||
for action in _.values(lank.thangType.getActions())
|
||||
continue unless _.any @defaultActions, (prefix) -> _.string.startsWith(action.name, prefix)
|
||||
continue unless _.any ThangType.defaultActions, (prefix) -> _.string.startsWith(action.name, prefix)
|
||||
@upsertActionToRender(lank.thangType, action.name, lank.options.colorConfig)
|
||||
|
||||
upsertActionToRender: (thangType, actionName, colorConfig) ->
|
||||
|
@ -346,17 +346,9 @@ module.exports = LayerAdapter = class LayerAdapter extends CocoClass
|
|||
#- Rendering containers for segmented thang types
|
||||
|
||||
renderSegmentedThangType: (thangType, colorConfig, actionNames, spriteSheetBuilder) ->
|
||||
containersToRender = {}
|
||||
for actionName in actionNames
|
||||
action = _.find(thangType.getActions(), {name: actionName})
|
||||
if action.container
|
||||
containersToRender[action.container] = true
|
||||
else if action.animation
|
||||
animationContainers = @getContainersForAnimation(thangType, action.animation, action)
|
||||
containersToRender[container.gn] = true for container in animationContainers
|
||||
|
||||
containersToRender = thangType.getContainersForActions(actionNames)
|
||||
spriteBuilder = new SpriteBuilder(thangType, {colorConfig: colorConfig})
|
||||
for containerGlobalName in _.keys(containersToRender)
|
||||
for containerGlobalName in containersToRender
|
||||
containerKey = @renderGroupingKey(thangType, containerGlobalName, colorConfig)
|
||||
if @spriteSheet?.resolutionFactor is @resolutionFactor and containerKey in @spriteSheet.getAnimations()
|
||||
container = new createjs.Sprite(@spriteSheet)
|
||||
|
@ -367,15 +359,6 @@ module.exports = LayerAdapter = class LayerAdapter extends CocoClass
|
|||
frame = spriteSheetBuilder.addFrame(container, null, @resolutionFactor * (thangType.get('scale') or 1))
|
||||
spriteSheetBuilder.addAnimation(containerKey, [frame], false)
|
||||
|
||||
getContainersForAnimation: (thangType, animation, action) ->
|
||||
rawAnimation = thangType.get('raw').animations[animation]
|
||||
if not rawAnimation
|
||||
console.error 'thang type', thangType.get('name'), 'is missing animation', animation, 'from action', action
|
||||
containers = rawAnimation.containers
|
||||
for animation in thangType.get('raw').animations[animation].animations
|
||||
containers = containers.concat(@getContainersForAnimation(thangType, animation.gn, action))
|
||||
return containers
|
||||
|
||||
#- Rendering sprite sheets for singular thang types
|
||||
|
||||
renderSingularThangType: (thangType, colorConfig, actionNames, spriteSheetBuilder) ->
|
||||
|
|
|
@ -35,6 +35,7 @@ module.exports = class ThangType extends CocoModel
|
|||
urlRoot: '/db/thang.type'
|
||||
building: {}
|
||||
editableByArtisans: true
|
||||
@defaultActions: ['idle', 'die', 'move', 'attack']
|
||||
|
||||
initialize: ->
|
||||
super()
|
||||
|
@ -78,6 +79,14 @@ module.exports = class ThangType extends CocoModel
|
|||
return {} unless @isFullyLoaded()
|
||||
return @actions or @buildActions()
|
||||
|
||||
getDefaultActions: ->
|
||||
actions = []
|
||||
for action in _.values(@getActions())
|
||||
continue unless _.any ThangType.defaultActions, (prefix) ->
|
||||
_.string.startsWith(action.name, prefix)
|
||||
actions.push(action)
|
||||
return actions
|
||||
|
||||
buildActions: ->
|
||||
return null unless @isFullyLoaded()
|
||||
@actions = $.extend(true, {}, @get('actions'))
|
||||
|
@ -479,3 +488,24 @@ module.exports = class ThangType extends CocoModel
|
|||
playerLevel = me.constructor.levelForTier playerTier
|
||||
#console.log 'Level required for', @get('name'), 'is', playerLevel, 'player tier', playerTier, 'because it is itemTier', itemTier, 'which is normally level', me.constructor.levelForTier(itemTier)
|
||||
playerLevel
|
||||
|
||||
getContainersForAnimation: (animation, action) ->
|
||||
rawAnimation = @get('raw').animations[animation]
|
||||
if not rawAnimation
|
||||
console.error 'thang type', @get('name'), 'is missing animation', animation, 'from action', action
|
||||
containers = rawAnimation.containers
|
||||
for animation in @get('raw').animations[animation].animations
|
||||
containers = containers.concat(@getContainersForAnimation(animation.gn, action))
|
||||
return containers
|
||||
|
||||
getContainersForActions: (actionNames) ->
|
||||
containersToRender = {}
|
||||
actions = @getActions()
|
||||
for actionName in actionNames
|
||||
action = _.find(actions, {name: actionName})
|
||||
if action.container
|
||||
containersToRender[action.container] = true
|
||||
else if action.animation
|
||||
animationContainers = @getContainersForAnimation(action.animation, action)
|
||||
containersToRender[container.gn] = true for container in animationContainers
|
||||
return _.keys(containersToRender)
|
||||
|
|
|
@ -169,6 +169,42 @@ _.extend ThangTypeSchema.properties,
|
|||
extendedName: {type: 'string', title: 'Extended Hero Name', description: 'The long form of the hero\'s name. Ex.: "Captain Anya Weston".'}
|
||||
unlockLevelName: {type: 'string', title: 'Unlock Level Name', description: 'The name of the level in which the hero is unlocked.'}
|
||||
tasks: c.array {title: 'Tasks', description: 'Tasks to be completed for this ThangType.'}, c.task
|
||||
spriteSheets: c.array {title: 'SpriteSheets'},
|
||||
c.object {title: 'SpriteSheet'},
|
||||
actionNames: { type: 'array' }
|
||||
animations:
|
||||
type: 'object'
|
||||
description: 'Third EaselJS SpriteSheet animations format'
|
||||
additionalProperties: {
|
||||
description: 'EaselJS animation'
|
||||
type: 'object'
|
||||
properties: {
|
||||
frames: { type: 'array' }
|
||||
next: { type: ['string', 'null'] }
|
||||
speed: { type: 'number' }
|
||||
}
|
||||
}
|
||||
colorConfig: c.colorConfig()
|
||||
colorLabel: { enum: ['red', 'green', 'blue'] }
|
||||
frames:
|
||||
type: 'array'
|
||||
description: 'Second EaselJS SpriteSheet frames format'
|
||||
items:
|
||||
type: 'array'
|
||||
items: [
|
||||
{ type: 'number', title: 'x' }
|
||||
{ type: 'number', title: 'y' }
|
||||
{ type: 'number', title: 'width' }
|
||||
{ type: 'number', title: 'height' }
|
||||
{ type: 'number', title: 'imageIndex' }
|
||||
{ type: 'number', title: 'regX' }
|
||||
{ type: 'number', title: 'regY' }
|
||||
]
|
||||
image: { type: 'string', format: 'image-file' }
|
||||
resolutionFactor: {
|
||||
type: 'number'
|
||||
}
|
||||
spriteType: { enum: ['singular', 'segmented'], title: 'Sprite Type' }
|
||||
|
||||
|
||||
ThangTypeSchema.required = []
|
||||
|
|
34
app/templates/editor/thang/export-thang-type-modal.jade
Normal file
34
app/templates/editor/thang/export-thang-type-modal.jade
Normal file
|
@ -0,0 +1,34 @@
|
|||
extends /templates/core/modal-base
|
||||
|
||||
block modal-header-content
|
||||
h4.modal-title Export #{view.thangType.get('name')} SpriteSheet
|
||||
|
||||
block modal-body-content
|
||||
.form-horizontal
|
||||
.form-group
|
||||
label.col-sm-3.control-label Team Color
|
||||
.col-sm-9
|
||||
select#color-config-select.form-control
|
||||
option(value='') None
|
||||
option(value="red") Red
|
||||
option(value="blue") Blue
|
||||
option(value="green") Green
|
||||
|
||||
.form-group
|
||||
label.col-sm-3.control-label Resolution Factor
|
||||
.col-sm-9
|
||||
input#resolution-input.form-control(value=3)
|
||||
.form-group
|
||||
label.col-sm-3.control-label Actions
|
||||
.col-sm-9
|
||||
- var defaultActionNames = _.pluck(view.thangType.getDefaultActions(), 'name')
|
||||
- var actions = view.thangType.getActions()
|
||||
for action in actions
|
||||
.checkbox
|
||||
label
|
||||
input(type="checkbox" name="action" value=action.name checked=_.contains(defaultActionNames, action.name))
|
||||
| #{action.name}
|
||||
|
||||
block modal-footer-content
|
||||
button.btn.btn-default(data-dismiss="modal") Cancel
|
||||
button#save-btn.btn.btn-primary Save
|
|
@ -79,19 +79,25 @@ block outer_content
|
|||
div.tab-pane#editor-thang-colors-tab-view
|
||||
div.tab-pane#editor-thang-main-tab-view.active
|
||||
div#settings-col.well
|
||||
img#portrait.img-thumbnail
|
||||
div.file-controls
|
||||
button(disabled=authorized === true ? undefined : "true").btn.btn-sm.btn-info#upload-button
|
||||
span.glyphicon.glyphicon-upload
|
||||
span.spl Upload Animation
|
||||
button(disabled=authorized === true ? undefined : "true").btn.btn-sm.btn-danger#clear-button
|
||||
span.glyphicon.glyphicon-remove
|
||||
span.spl Clear Data
|
||||
button#set-vector-icon(disabled=authorized === true ? undefined : "true").btn.btn-sm
|
||||
span.glyphicon.glyphicon-gbp
|
||||
span.spl Vector Icon Setup
|
||||
input#real-upload-button(type="file")
|
||||
#thang-type-file-size= fileSizeString
|
||||
.row
|
||||
.col-sm-3
|
||||
img#portrait.img-thumbnail
|
||||
.col-sm-9
|
||||
div.file-controls
|
||||
button(disabled=authorized === true ? undefined : "true").btn.btn-sm.btn-info#upload-button
|
||||
span.glyphicon.glyphicon-upload
|
||||
span.spl Upload Animation
|
||||
button(disabled=authorized === true ? undefined : "true").btn.btn-sm.btn-danger#clear-button
|
||||
span.glyphicon.glyphicon-remove
|
||||
span.spl Clear Data
|
||||
button#set-vector-icon(disabled=authorized === true ? undefined : "true").btn.btn-sm
|
||||
span.glyphicon.glyphicon-gbp
|
||||
span.spl Vector Icon Setup
|
||||
button#export-sprite-sheet-btn.btn.btn-sm(disabled=authorized === true ? undefined : "true")
|
||||
span.glyphicon.glyphicon-export
|
||||
span.spl Export SpriteSheet
|
||||
input#real-upload-button(type="file")
|
||||
#thang-type-file-size= fileSizeString
|
||||
div#thang-type-treema
|
||||
.clearfix
|
||||
div#display-col.well
|
||||
|
|
78
app/views/editor/thang/ExportThangTypeModal.coffee
Normal file
78
app/views/editor/thang/ExportThangTypeModal.coffee
Normal file
|
@ -0,0 +1,78 @@
|
|||
ModalView = require 'views/core/ModalView'
|
||||
template = require 'templates/editor/thang/export-thang-type-modal'
|
||||
SpriteExporter = require 'lib/sprites/SpriteExporter'
|
||||
|
||||
module.exports = class ExportThangTypeModal extends ModalView
|
||||
id: "export-thang-type-modal"
|
||||
template: template
|
||||
plain: true
|
||||
|
||||
events:
|
||||
'click #save-btn': 'onClickSaveButton'
|
||||
|
||||
initialize: (options, @thangType) ->
|
||||
@builder = null
|
||||
@getFilename = _.once(@getFilename)
|
||||
|
||||
colorMap: {
|
||||
red: { hue: 0, saturation: 0.75, lightness: 0.5 }
|
||||
blue: { hue: 0.66, saturation: 0.75, lightness: 0.5 }
|
||||
green: { hue: 0.33, saturation: 0.75, lightness: 0.5 }
|
||||
}
|
||||
getColorLabel: -> @$('#color-config-select').val()
|
||||
getColorConfig: -> @colorMap[@getColorLabel()]
|
||||
getActionNames: -> _.map @$('input[name="action"]:checked'), (el) -> $(el).val()
|
||||
getResolutionFactor: -> parseInt(@$('#resolution-input').val()) or SPRITE_RESOLUTION_FACTOR
|
||||
getFilename: -> 'spritesheet-'+_.string.slugify(moment().format())+'.png'
|
||||
|
||||
onClickSaveButton: ->
|
||||
options = {
|
||||
resolutionFactor: @getResolutionFactor()
|
||||
actionNames: @getActionNames()
|
||||
colorConfig: @getColorConfig()
|
||||
}
|
||||
console.log 'options?', options
|
||||
@exporter = new SpriteExporter(@thangType, options)
|
||||
@exporter.build()
|
||||
@listenToOnce @exporter, 'build', @onExporterBuild
|
||||
|
||||
onExporterBuild: (e) ->
|
||||
@spriteSheet = e.spriteSheet
|
||||
$('body').empty().append(@spriteSheet._images[0])
|
||||
return
|
||||
src = @spriteSheet._images[0].toDataURL()
|
||||
src = src.replace('data:image/png;base64,', '').replace(/\ /g, '+')
|
||||
body =
|
||||
filename: @getFilename()
|
||||
mimetype: 'image/png'
|
||||
path: "db/thang.type/#{@thangType.get('original')}"
|
||||
b64png: src
|
||||
$.ajax('/file', {type: 'POST', data: body, success: @onSpriteSheetUploaded})
|
||||
|
||||
onSpriteSheetUploaded: =>
|
||||
spriteSheetData = {
|
||||
actionNames: @getActionNames()
|
||||
animations: @spriteSheet._data
|
||||
frames: ([
|
||||
f.rect.x
|
||||
f.rect.y
|
||||
f.rect.width
|
||||
f.rect.height
|
||||
0
|
||||
f.regX
|
||||
f.regY
|
||||
] for f in @spriteSheet._frames)
|
||||
image: "db/thang.type/#{@thangType.get('original')}/"+@getFilename()
|
||||
resolutionFactor: @getResolutionFactor()
|
||||
}
|
||||
if config = @getColorConfig()
|
||||
spriteSheetData.colorConfig = config
|
||||
if label = @getColorLabel()
|
||||
spriteSheetData.colorLabel = label
|
||||
spriteSheets = _.clone(@thangType.get('spriteSheets') or [])
|
||||
spriteSheets.push(spriteSheetData)
|
||||
@thangType.set('spriteSheets', spriteSheets)
|
||||
@thangType.save()
|
||||
@listenToOnce @thangType, 'sync', @hide
|
||||
|
||||
window.SomeModal = module.exports
|
|
@ -20,6 +20,7 @@ VectorIconSetupModal = require 'views/editor/thang/VectorIconSetupModal'
|
|||
SaveVersionModal = require 'views/editor/modal/SaveVersionModal'
|
||||
template = require 'templates/editor/thang/thang-type-edit-view'
|
||||
storage = require 'core/storage'
|
||||
ExportThangTypeModal = require './ExportThangTypeModal'
|
||||
|
||||
CENTER = {x: 200, y: 400}
|
||||
|
||||
|
@ -157,6 +158,7 @@ module.exports = class ThangTypeEditView extends RootView
|
|||
'mousedown #canvas': 'onCanvasMouseDown'
|
||||
'mouseup #canvas': 'onCanvasMouseUp'
|
||||
'mousemove #canvas': 'onCanvasMouseMove'
|
||||
'click #export-sprite-sheet-btn': 'onClickExportSpriteSheetButton'
|
||||
|
||||
onClickSetVectorIcon: ->
|
||||
modal = new VectorIconSetupModal({}, @thangType)
|
||||
|
@ -208,6 +210,7 @@ module.exports = class ThangTypeEditView extends RootView
|
|||
@patchesView = @insertSubView(new PatchesView(@thangType), @$el.find('.patches-view'))
|
||||
@showReadOnly() if me.get('anonymous')
|
||||
@updatePortrait()
|
||||
@onClickExportSpriteSheetButton()
|
||||
|
||||
initComponents: =>
|
||||
options =
|
||||
|
@ -651,6 +654,10 @@ module.exports = class ThangTypeEditView extends RootView
|
|||
@canvasDragOffset = null
|
||||
node.set '/', offset
|
||||
|
||||
onClickExportSpriteSheetButton: ->
|
||||
modal = new ExportThangTypeModal({}, @thangType)
|
||||
@openModalView(modal)
|
||||
|
||||
destroy: ->
|
||||
@camera?.destroy()
|
||||
super()
|
||||
|
|
|
@ -35,6 +35,7 @@ ThangTypeHandler = class ThangTypeHandler extends Handler
|
|||
'unlockLevelName'
|
||||
'tasks'
|
||||
'terrains'
|
||||
'spriteSheets'
|
||||
]
|
||||
|
||||
hasAccess: (req) ->
|
||||
|
|
Loading…
Reference in a new issue