// // Nenya library - tools for developing networked games // Copyright (C) 2002-2010 Three Rings Design, Inc., All Rights Reserved // http://www.threerings.net/code/nenya/ // // This library is free software; you can redistribute it and/or modify it // under the terms of the GNU Lesser General Public License as published // by the Free Software Foundation; either version 2.1 of the License, or // (at your option) any later version. // // This library is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU // Lesser General Public License for more details. // // You should have received a copy of the GNU Lesser General Public // License along with this library; if not, write to the Free Software // Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA package com.threerings.cast { import flash.geom.Point; import com.threerings.util.Hashable; import com.threerings.util.Log; import com.threerings.util.Map; import com.threerings.util.StringUtil; import com.threerings.util.Maps; import com.threerings.util.maps.LRMap; /** * The character manager provides facilities for constructing sprites that * are used to represent characters in a scene. It also handles the * compositing and caching of composited character animations. */ public class CharacterManager { private static var log :Log = Log.getLog(CharacterManager); /** * Constructs the character manager. */ public function CharacterManager (crepo :ComponentRepository) { // keep this around _crepo = crepo; for each (var action :ActionSequence in crepo.getActionSequences()) { _actions.put(action.name, action); } } /** * Returns a {@link CharacterSprite} representing the character * described by the given {@link CharacterDescriptor}, or * null if an error occurs. * * @param desc the character descriptor. */ public function getCharacter (desc :CharacterDescriptor, charClass :Class = null) :CharacterSprite { if (charClass == null) { charClass = _charClass; } try { var sprite :CharacterSprite = new charClass(); sprite.init(desc, this); return sprite; } catch (e :Error) { log.warning("Failed to instantiate character sprite.", e); return null; } return getCharacter(desc, charClass); } public function getActionSequence (action :String) :ActionSequence { return _actions.get(action); } public function getActionFrames (descrip :CharacterDescriptor, action :String) :ActionFrames { if (!isLoaded(descrip)) { return null; } var key :FrameKey = new FrameKey(descrip, action); var frames :ActionFrames = _actionFrames.get(key); if (frames == null) { // this doesn't actually composite the images, but prepares an // object to be able to do so frames = createCompositeFrames(descrip, action); _actionFrames.put(key, frames); } return frames; } /** * Returns whether all the components are loaded and ready to go. */ protected function isLoaded (descrip :CharacterDescriptor) :Boolean { var cids :Array = descrip.getComponentIds(); var ccount :int = cids.length; for (var ii :int = 0; ii < ccount; ii++) { var ccomp :CharacterComponent = _crepo.getComponent(cids[ii]); if (!ccomp.isLoaded()) { return false; } } return true; } public function load (descrip :CharacterDescriptor, notify :Function) :void { _crepo.load(descrip.getComponentIds(), notify); } protected function createCompositeFrames (descrip :CharacterDescriptor, action :String) :ActionFrames { var cids :Array = descrip.getComponentIds(); var ccount :int = cids.length; var zations :Array = descrip.getColorizations(); var xlations :Array = descrip.getTranslations(); log.debug("Compositing action [action=" + action + ", descrip=" + descrip + "]."); // this will be used to construct any shadow layers var shadows :Map = null; /* of String, Array */ // maps components by class name for masks var ccomps :Map = Maps.newMapOf(String); /* of String, ArrayList */ // create colorized versions of all of the source action frames var sources :Array = []; for (var ii :int = 0; ii < ccount; ii++) { var cframes :ComponentFrames = new ComponentFrames(); sources.push(cframes); var ccomp :CharacterComponent = cframes.ccomp = _crepo.getComponent(cids[ii]); // load up the main component images var source :ActionFrames = ccomp.getFrames(action, null); if (source == null) { var errmsg :String = "Cannot composite action frames; no such " + "action for component [action=" + action + ", desc=" + descrip + ", comp=" + ccomp + "]"; throw new Error(errmsg); } source = (zations == null || zations[ii] == null) ? source : source.cloneColorized(zations[ii]); var xlation :Point = (xlations == null) ? null : xlations[ii]; cframes.frames = (xlation == null) ? source : source.cloneTranslated(xlation.x, xlation.y); // store the component with its translation under its class for masking var tcomp :TranslatedComponent = new TranslatedComponent(ccomp, xlation); var tcomps :Array = ccomps.get(ccomp.componentClass.name); if (tcomps == null) { ccomps.put(ccomp.componentClass.name, tcomps = []); } tcomps.push(tcomp); // if this component has a shadow, make a note of it if (ccomp.componentClass.isShadowed()) { if (shadows == null) { shadows = Maps.newMapOf(String); } var shadlist :Array = shadows.get(ccomp.componentClass.shadow); if (shadlist == null) { shadows.put(ccomp.componentClass.shadow, shadlist = []); } shadlist.push(tcomp); } } /* TODO - Implement shadows & masks - I don't actually know if/where we use these, though. // now create any necessary shadow layers if (shadows != null) { for (Map.Entry> entry : shadows.entrySet()) { ComponentFrames scf = compositeShadow(action, entry.getKey(), entry.getValue()); if (scf != null) { sources.add(scf); } } } // add any necessary masks for (ComponentFrames cframes : sources) { ArrayList mcomps = ccomps.get(cframes.ccomp.componentClass.mask); if (mcomps != null) { cframes.frames = compositeMask(action, cframes.ccomp, cframes.frames, mcomps); } } */ // use those to create an entity that will lazily composite things // together as they are needed return new CompositedActionFrames(_frameCache, _actions.get(action), sources); } protected var _crepo :ComponentRepository; protected var _actions :Map = Maps.newMapOf(String); protected var _actionFrames :Map = Maps.newMapOf(FrameKey); protected var _frameCache :LRMap = new LRMap(Maps.newMapOf(CompositedFramesKey), MAX_FRAMES); protected var _charClass :Class; protected static const MAX_FRAMES :int = 1000; } } import com.threerings.util.Hashable; import com.threerings.util.StringUtil; import com.threerings.cast.CharacterDescriptor; class FrameKey implements Hashable { public var desc :CharacterDescriptor; public var action :String; public function FrameKey (desc :CharacterDescriptor, action :String) { this.desc = desc; this.action = action; } public function hashCode () :int { return desc.hashCode() ^ StringUtil.hashCode(action); } public function equals (other :Object) :Boolean { if (other is FrameKey) { var okey :FrameKey = FrameKey(other); return okey.desc.equals(desc) && okey.action == action; } else { return false; } } } import flash.geom.Point; import com.threerings.cast.ActionFrames; import com.threerings.cast.CharacterComponent; class TranslatedComponent { public var ccomp :CharacterComponent; public var xlation :Point; public function TranslatedComponent (ccomp :CharacterComponent, xlation :Point) { this.ccomp = ccomp; this.xlation = xlation; } public function getFrames (action :String, type :String) :ActionFrames { var frames :ActionFrames = ccomp.getFrames(action, type); return (frames == null || xlation == null) ? frames : frames.cloneTranslated(xlation.x, xlation.y); } }