Split Nenya into proper Maven submodules (same treatment Narya got).

This commit is contained in:
Michael Bayne
2012-02-27 11:46:22 -08:00
parent 90146d517d
commit 5e2380eb24
645 changed files with 358 additions and 283 deletions
@@ -0,0 +1,73 @@
//
// $Id$
//
// Nenya library - tools for developing networked games
// Copyright (C) 2002-2012 Three Rings Design, Inc., All Rights Reserved
// http://code.google.com/p/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.display.DisplayObject;
/**
* Encapsulates a set of frames in each of {@link
* DirectionCodes#DIRECTION_COUNT} orientations that are used to render a
* character sprite.
*/
public interface ActionFrames
{
/**
* Returns the number of orientations available in this set of action
* frames.
*/
function getOrientationCount () :int;
/**
* Returns the multi-frame image that comprises the frames for the
* specified orientation.
*/
function getFrames (orient :int, callback :Function) :void;
/**
* Returns the x offset from the upper left of the image to the
* "origin" for this character frame. A sprite with location (x, y)
* will be rendered such that its origin is coincident with that
* location.
*/
function getXOrigin (orient :int, frameIdx :int) :int;
/**
* Returns the y offset from the upper left of the image to the
* "origin" for this character frame. A sprite with location (x, y)
* will be rendered such that its origin is coincident with that
* location.
*/
function getYOrigin (orient :int, frameIdx :int) :int;
/**
* Creates a clone of these action frames which will have the supplied
* colorizations applied to the frame images.
*/
function cloneColorized (zations :Array) :ActionFrames;
/**
* Creates a clone of these action frames which will have the supplied
* translation applied to the frame images.
*/
function cloneTranslated (dx :int, dy :int) :ActionFrames;
}
}
@@ -0,0 +1,108 @@
//
// $Id$
//
// Nenya library - tools for developing networked games
// Copyright (C) 2002-2012 Three Rings Design, Inc., All Rights Reserved
// http://code.google.com/p/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.media.tile.SwissArmyTileSet;
import com.threerings.util.DirectionUtil;
import com.threerings.util.StringUtil;
/**
* The action sequence class describes a particular character animation
* sequence. An animation sequence consists of one or more frames of
* animation, renders at a particular frame rate, and has an origin point
* that specifies the location of the base of the character in relation to
* the bounds of the animation images.
*/
public class ActionSequence
{
/**
* Defines the name of the default action sequence. When component
* tilesets are loaded to build a set of composited images for a
* particular action sequence, a check is first made for a component
* tileset specific to the action sequence and then for the
* component's default tileset if the action specific tileset did not
* exist.
*/
public static const DEFAULT_SEQUENCE :String = "default";
/** The action sequence name. */
public var name :String;
/** The number of frames per second to show when animating. */
public var framesPerSecond :Number;
/** The position of the character's base for this sequence. */
public var origin :Point = new Point();
public var tileset :SwissArmyTileSet;
/** Orientation codes for the orientations available for this
* action. */
public var orients :Array;
public static function fromXml (xml :XML) :ActionSequence
{
var seq :ActionSequence = new ActionSequence();
seq.name = xml.@name;
seq.framesPerSecond = xml.framesPerSecond;
seq.orients = toOrientArray(xml.orients);
seq.origin = toPoint(xml.origin);
seq.tileset = SwissArmyTileSet.fromXml(xml.tileset[0]);
return seq;
}
protected static function toOrientArray (str :String) :Array
{
if (str == null || str.length == 0) {
return null;
}
return str.split(",").map(function(element :String, index :int, arr :Array) :int {
return DirectionUtil.fromShortString(StringUtil.trim(element));
});
}
protected static function toPoint (str :String) :Point
{
if (str == null || str.length == 0) {
return null;
}
var coords :Array =
str.split(",").map(function(element :String, index :int, arr :Array) :int {
return int(StringUtil.trim(element));
});
return new Point(coords[0], coords[1]);
}
public function toString () :String
{
return "[name=" + name + ", framesPerSecond=" + framesPerSecond +
", origin=" + origin +
", orients=" + (orients == null ? 0 : orients.length) + "]";
}
}
}
@@ -0,0 +1,145 @@
//
// $Id$
//
// Nenya library - tools for developing networked games
// Copyright (C) 2002-2012 Three Rings Design, Inc., All Rights Reserved
// http://code.google.com/p/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 com.threerings.util.Hashable;
import com.threerings.util.Equalable;
import com.threerings.util.Set;
import com.threerings.util.StringUtil;
/**
* The character component represents a single component that can be composited with other
* character components to generate an image representing a complete character displayable in any
* of the eight compass directions as detailed in the {@link Sprite} class direction constants.
*/
public class CharacterComponent
implements Equalable, Hashable
{
/** The unique component identifier. */
public var componentId :int;
/** The component's name. */
public var name :String;
/** The class of components to which this one belongs. */
public var componentClass :ComponentClass;
/**
* Constructs a character component with the specified id of the specified class.
*/
public function CharacterComponent (
componentId :int, name :String, compClass :ComponentClass, fprov :FrameProvider)
{
this.componentId = componentId;
this.name = name;
this.componentClass = compClass;
_frameProvider = fprov;
}
public function setFrameProvider (frameProvider :FrameProvider) :void
{
_frameProvider = frameProvider;
// Notify them all and clear our list.
for each (var func :Function in _notifyOnLoad) {
func(this);
}
_notifyOnLoad = [];
}
/**
* Returns the render priority appropriate for this component at the specified action and
* orientation.
*/
public function getRenderPriority (action :String, orientation :int) :int
{
return componentClass.getRenderPriority(action, name, orientation);
}
/**
* Returns the image frames for the specified action animation or null if no animation for the
* specified action is available for this component.
*
* @param type null for the normal action frames or one of the custom action sub-types:
* {@link StandardActions#SHADOW_TYPE}, etc.
*/
public function getFrames (action :String, type :String) :ActionFrames
{
return isLoaded() ? _frameProvider.getFrames(this, action, type) : null;
}
/**
* Returns the path to the image frames for the specified action animation or null if no
* animation for the specified action is available for this component.
*
* @param type null for the normal action frames or one of the custom action sub-types:
* {@link StandardActions#SHADOW_TYPE}, etc.
*
* @param existentPaths the set of all paths for which there are valid frames.
*/
public function getFramePath (action :String, type :String, existentPaths :Set) :String
{
return _frameProvider.getFramePath(this, action, type, existentPaths);
}
public function equals (other :Object) :Boolean
{
if (other is CharacterComponent) {
return componentId == (CharacterComponent(other)).componentId;
} else {
return false;
}
}
public function hashCode () :int
{
return componentId;
}
public function toString () :String
{
return StringUtil.simpleToString(this);
}
public function isLoaded () :Boolean
{
return _frameProvider != null;
}
public function notifyOnLoad (func :Function) :void
{
if (isLoaded()) {
func(this);
} else {
_notifyOnLoad.push(func);
}
}
/** The entity from which we obtain our animation frames. */
protected var _frameProvider :FrameProvider;
/** Everyone who cares when we're loaded. */
protected var _notifyOnLoad :Array = [];
}
}
@@ -0,0 +1,160 @@
//
// $Id$
//
// Nenya library - tools for developing networked games
// Copyright (C) 2002-2012 Three Rings Design, Inc., All Rights Reserved
// http://code.google.com/p/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 com.threerings.media.image.Colorization;
import com.threerings.util.Arrays;
import com.threerings.util.Equalable;
import com.threerings.util.Hashable;
import com.threerings.util.StringUtil;
/**
* The character descriptor object details the components that are
* pieced together to create a single character image.
*/
public class CharacterDescriptor
implements Equalable, Hashable
{
/**
* Constructs a character descriptor.
*
* @param components the component ids of the individual components
* that make up this character.
* @param zations the colorizations to apply to each of the character
* component images when compositing actions (an array of
* colorizations for each component, elements of which can be null to
* make it easier to support per-component specialized colorizations).
* This can be null if image recolorization is not desired.
*/
public function CharacterDescriptor (components :Array, zations :Array)
{
_components = components;
_zations = zations;
}
/**
* Returns an array of the component identifiers comprising the
* character described by this descriptor.
*/
public function getComponentIds () :Array
{
return _components;
}
/**
* Returns an array of colorization arrays to be applied to the
* components when compositing action images (one array per
* component).
*/
public function getColorizations () :Array
{
return _zations;
}
/**
* Updates the colorizations to be used by this character descriptor.
*/
public function setColorizations (zations :Array) :void
{
_zations = zations;
}
/**
* Returns the array of translations to be applied to the components
* when compositing action images.
*/
public function getTranslations () :Array
{
return _xlations;
}
/**
* Updates the translations to be used by this character descriptor.
*/
public function setTranslations (xlations :Array) :void
{
_xlations = xlations;
}
public function hashCode () :int
{
var code :int = 0;
var clength :int = _components.length;
for (var ii :int = 0; ii < clength; ii++) {
code ^= _components[ii];
}
return code;
}
public function equals (other :Object) :Boolean
{
if (!(other is CharacterDescriptor)) {
return false;
}
// both the component ids and the colorizations must be equal
var odesc :CharacterDescriptor = CharacterDescriptor(other);
if (!Arrays.equals(_components, odesc._components)) {
return false;
}
var zations :Array = odesc._zations;
if (zations == null && _zations == null) {
// if neither has colorizations, we're clear
return true;
} else if (zations == null || _zations == null) {
// if one has colorizations whilst the other doesn't, they
// can't be equal
return false;
}
// otherwise, all of the colorizations must be equal as well
var zlength :int= zations.length;
if (zlength != _zations.length) {
return false;
}
for (var ii :int= 0; ii < zlength; ii++) {
if (!Arrays.equals(_zations[ii], zations[ii])) {
return false;
}
}
return Arrays.equals(_xlations, odesc._xlations);
}
public function toString () :String
{
return "[cids=" + StringUtil.toString(_components) +
", colors=" + StringUtil.toString(_zations) + "]";
}
/** The component identifiers comprising the character. */
protected var _components :Array;
/** The colorizations to apply when compositing this character. */
protected var _zations :Array;
/** The translations to apply when compositing this character. */
protected var _xlations :Array;
}
}
@@ -0,0 +1,292 @@
//
// $Id$
//
// Nenya library - tools for developing networked games
// Copyright (C) 2002-2012 Three Rings Design, Inc., All Rights Reserved
// http://code.google.com/p/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
* <code>null</code> 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 getComponent (compId :int) :CharacterComponent
{
return _crepo.getComponent(compId);
}
public function getComponentByName (cclass :String, cname :String) :CharacterComponent
{
return _crepo.getComponentByName(cclass, cname);
}
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<TranslatedComponent> */
// maps components by class name for masks
var ccomps :Map = Maps.newMapOf(String); /* of String, ArrayList<TranslatedComponent> */
// 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<String, ArrayList<TranslatedComponent>> 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<TranslatedComponent> 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);
}
}
@@ -0,0 +1,317 @@
//
// $Id$
//
// Nenya library - tools for developing networked games
// Copyright (C) 2002-2012 Three Rings Design, Inc., All Rights Reserved
// http://code.google.com/p/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.display.DisplayObject;
import flash.display.Sprite;
import flash.geom.Point;
import flash.geom.Rectangle;
import com.threerings.display.DisplayUtil;
import com.threerings.media.Tickable;
import com.threerings.util.DirectionCodes;
import com.threerings.util.Log;
/**
* A character sprite is a sprite that animates itself while walking
* about in a scene.
*/
public class CharacterSprite extends Sprite
implements Tickable
{
private static var log :Log = Log.getLog(CharacterSprite);
/**
* Initializes this character sprite with the specified character
* descriptor and character manager. It will obtain animation data
* from the supplied character manager.
*/
public function init (descrip :CharacterDescriptor, charmgr :CharacterManager) :void
{
// keep track of this stuff
_descrip = descrip;
_charmgr = charmgr;
// sanity check our values
sanityCheckDescrip();
// assign an arbitrary starting orientation
_orient = DirectionCodes.SOUTHWEST;
// pass the buck to derived classes
didInit();
halt();
updateActionFrames();
}
public function tick (tickStamp :int) :void
{
if (_framesBitmap != null) {
_framesBitmap.tick(tickStamp);
}
}
/**
* Called after this sprite has been initialized with its character
* descriptor and character manager. Derived classes can do post-init
* business here.
*/
protected function didInit () :void
{
_mainSprite = new Sprite();
addChild(_mainSprite);
}
/**
* Reconfigures this sprite to use the specified character descriptor.
*/
public function setCharacterDescriptor (descrip :CharacterDescriptor) :void
{
// keep the new descriptor
_descrip = descrip;
// sanity check our values
sanityCheckDescrip();
// update our action frames
updateActionFrames();
}
/**
* Specifies the action to use when the sprite is at rest. The default
* is <code>STANDING</code>.
*/
public function setRestingAction (action :String) :void
{
_restingAction = action;
}
/**
* Returns the action to be used when the sprite is at rest. Derived
* classes may wish to override this method and vary the action based
* on external parameters (or randomly).
*/
public function getRestingAction () :String
{
return _restingAction;
}
/**
* Specifies the action to use when the sprite is following a path.
* The default is <code>WALKING</code>.
*/
public function setFollowingPathAction (action :String) :void
{
_followingPathAction = action;
}
/**
* Returns the action to be used when the sprite is following a path.
* Derived classes may wish to override this method and vary the
* action based on external parameters (or randomly).
*/
public function getFollowingPathAction () :String
{
return _followingPathAction;
}
/**
* Sets the action sequence used when rendering the character, from
* the set of available sequences.
*/
public function setActionSequence (action :String) :void
{
// sanity check
if (action == null) {
log.warning("Refusing to set null action sequence " + this + ".", new Error());
return;
}
// no need to noop
if (action == _action) {
return;
}
_action = action;
updateActionFrames();
}
public function setOrientation (orient :int) :void
{
var oorient :int = _orient;
_orient = orient;
if (orient < 0 || orient >= DirectionCodes.FINE_DIRECTION_COUNT) {
log.info("Refusing to set invalid orientation [sprite=" + this +
", orient=" + orient + "].", new Error());
return;
}
if (_orient != oorient) {
updateMainSprite();
}
}
public function getOrientation () :int
{
return _orient;
}
public function cancelMove () :void
{
halt();
}
public function pathBeginning () :void
{
// enable walking animation
setActionSequence(getFollowingPathAction());
}
public function pathCompleted (timestamp :int) :void
{
halt();
}
/**
* Rebuilds our action frames given our current character descriptor
* and action sequence. This is called when either of those two things
* changes.
*/
protected function updateActionFrames () :void
{
// get a reference to the action sequence so that we can obtain
// our animation frames and configure our frames per second
var actseq :ActionSequence = _charmgr.getActionSequence(_action);
if (actseq == null) {
var errmsg :String = "No such action '" + _action + "'.";
throw new Error(errmsg);
}
try {
// obtain our animation frames for this action sequence
_aframes = _charmgr.getActionFrames(_descrip, _action);
if (_aframes == null) {
// Once the frames are really ready to go, we'll do this...
_charmgr.load(_descrip, updateActionFrames);
}
updateMainSprite();
} catch (nsce :NoSuchComponentError) {
log.warning("Character sprite references non-existent " +
"component [sprite=" + this + ", err=" + nsce + "].");
} catch (e :Error) {
log.warning("Failed to obtain action frames [sprite=" + this +
", descrip=" + _descrip + ", action=" + _action + "].", e);
}
x = -actseq.origin.x;
y = -actseq.origin.y;
}
protected function updateMainSprite () :void
{
DisplayUtil.removeAllChildren(_mainSprite);
if (_aframes != null) {
updateWithUnloadedSprite();
var curFrames :ActionFrames = _aframes;
_aframes.getFrames(_orient, function(frames :MultiFrameBitmap) :void {
// Ensure our action hasn't been swapped out on us since we started loading.
if (_aframes == curFrames) {
_framesBitmap = frames;
DisplayUtil.removeAllChildren(_mainSprite);
_mainSprite.addChild(frames);
}
});
} else {
_framesBitmap = null;
updateWithUnloadedSprite();
}
}
protected function updateWithUnloadedSprite () :void
{
// Nothing by default.
}
/**
* Makes it easier to track down problems with bogus character descriptors.
*/
protected function sanityCheckDescrip () :void
{
if (_descrip.getComponentIds() == null ||
_descrip.getComponentIds().length == 0) {
log.warning("Invalid character descriptor [sprite=" + this +
", descrip=" + _descrip + "].", new Error());
}
}
/**
* Updates the sprite animation frame to reflect the cessation of
* movement and disables any further animation.
*/
protected function halt () :void
{
var rest :String = getRestingAction();
if (rest != null) {
setActionSequence(rest);
}
}
public function hitTest (stageX :int, stageY :int) :Boolean
{
return _framesBitmap == null ? false : _framesBitmap.hitTest(stageX, stageY);
}
/** The action to use when at rest. */
protected var _restingAction :String = StandardActions.STANDING;
/** The action to use when following a path. */
protected var _followingPathAction :String = StandardActions.WALKING;
/** A reference to the descriptor for the character that we're
* visualizing. */
protected var _descrip :CharacterDescriptor;
/** A reference to the character manager that created us. */
protected var _charmgr :CharacterManager;
/** The action we are currently displaying. */
protected var _action :String;
/** The animation frames for the active action sequence in each
* orientation. */
protected var _aframes :ActionFrames;
/** The currently active set of bitmaps for this character. */
protected var _framesBitmap :MultiFrameBitmap;
/** The orientation of this sprite. */
protected var _orient :int = DirectionCodes.NONE;
protected var _mainSprite :Sprite;
}
}
@@ -0,0 +1,198 @@
//
// $Id$
//
// Nenya library - tools for developing networked games
// Copyright (C) 2002-2012 Three Rings Design, Inc., All Rights Reserved
// http://code.google.com/p/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 com.threerings.util.Arrays;
import com.threerings.util.Hashable;
import com.threerings.util.Equalable;
import com.threerings.util.StringUtil;
/**
* Denotes a class of components to which {@link CharacterComponent}
* objects belong. Examples include "Hat", "Head", and "Feet". A component
* class dictates a component's rendering priority so that components can
* be rendered in an order that causes them to overlap properly.
*
* <p> Components support render priority overrides for particular
* actions, orientations or combinations of actions and orientations. The
* system is currently structured with the expectation that the overrides
* will be relatively few (less than fifteen, say) for any given component
* class. A system that relied on many overrides for its components would
* want to implement a more scalable algorithm for determining which, if
* any, override matches a particular action and orientation combination.
*/
public class ComponentClass
implements Equalable, Hashable
{
/** The component class name. */
public var name :String;
/** The default render priority. */
public var renderPriority :int;
/** The color classes to use when recoloring components of this class. May
* be null if a system does not use recolorable components. */
public var colors :Array;
/** The class name of the layer from which this component class obtains a
* mask to limit rendering to certain areas. */
public var mask :String;
/** Indicates the class name of the shadow layer to which this component
* class contributes a shadow. */
public var shadow :String;
/** 1.0 for a normal component, the alpha value of the pre-composited
* shadow for the special "shadow" component class. */
public var shadowAlpha :Number = 1.0;
/** Whether or not components of this class will have translations applied. */
public var translate :Boolean;
/**
* Creates an uninitialized instance suitable for unserialization or
* population during XML parsing.
*/
public function ComponentClass ()
{
}
public static function fromXml (xml :XML) :ComponentClass
{
var compClass :ComponentClass = new ComponentClass();
compClass.name = xml.@name;
compClass.renderPriority = xml.@renderPriority;
compClass.colors = toStrArray(xml.@colors);
compClass.mask = xml.@mask;
compClass.shadow = xml.@shadow;
compClass.shadowAlpha = xml.@shadowAlpha;
compClass.translate = xml.@translate;
for each (var overrideXml :XML in xml.override) {
compClass.addPriorityOverride(PriorityOverride.fromXml(overrideXml));
}
return compClass;
}
protected static function toStrArray (str :String) :Array
{
if (str == null || str.length == 0) {
return null;
}
return str.split(",").map(function(element :String, index :int, arr :Array) :String {
return StringUtil.trim(element);
});
}
/**
* Returns the render priority appropriate for the specified action, orientation and
* component.
*/
public function getRenderPriority (action :String, component :String, orientation :int) :int
{
// because we expect there to be relatively few priority overrides, we simply search
// linearly through the list for the closest match
var ocount :int = (_overrides != null) ? _overrides.length : 0;
for (var ii :int = 0; ii < ocount; ii++) {
var over :PriorityOverride = _overrides[ii];
// based on the way the overrides are sorted, the first match
// is the most specific and the one we want
if (over.matches(action, component, orientation)) {
return over.renderPriority;
}
}
return renderPriority;
}
/**
* Adds the supplied render priority override record to this component class.
*/
public function addPriorityOverride (override :PriorityOverride) :void
{
if (_overrides == null) {
_overrides = [];
}
Arrays.sortedInsert(_overrides, override);
}
/**
* Returns true if this component class contributes a shadow to a
* particular shadow layer. Note: this is different from <em>being</em> a
* shadow layer which is determined by calling {@link #isShadow}.
*/
public function isShadowed () :Boolean
{
return (shadow != null);
}
/**
* Returns true if this component class is a shadow layer rather than a normal component class.
*/
public function isShadow () :Boolean
{
return (shadowAlpha != 1.0);
}
/**
* Classes with the same name are the same.
*/
public function equals (other :Object) :Boolean
{
if (other is ComponentClass) {
return name == (ComponentClass(other)).name;
} else {
return false;
}
}
/**
* Hashcode is based on component class name.
*/
public function hashCode () :int
{
return StringUtil.hashCode(name);
}
public function toString () :String
{
var buf :String = "[";
buf = buf.concat("name=", name);
buf = buf.concat(", pri=", renderPriority);
if (colors != null) {
buf = buf.concat(", colors=", StringUtil.toString(colors));
}
if (mask != null) {
buf = buf.concat(", mask=", mask);
}
if (shadowAlpha != 1.0) {
buf = buf.concat(", shadow=", shadowAlpha);
} else if (shadow != null) {
buf = buf.concat(", shadow=", shadow);
}
return buf.concat("]");
}
/** A list of render priority overrides. */
protected var _overrides :Array = [];
}
}
@@ -0,0 +1,163 @@
//
// $Id$
//
// Nenya library - tools for developing networked games
// Copyright (C) 2002-2012 Three Rings Design, Inc., All Rights Reserved
// http://code.google.com/p/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.display.Bitmap;
import flash.display.BitmapData;
import flash.display.DisplayObject;
import flash.geom.Point;
import flash.geom.Rectangle;
import flash.events.Event;
import flash.utils.ByteArray;
import nochump.util.zip.ZipEntry;
import nochump.util.zip.ZipError;
import nochump.util.zip.ZipFile;
import com.threerings.media.image.PngRecolorUtil;
import com.threerings.media.tile.TileDataPack;
import com.threerings.media.tile.TileSet;
import com.threerings.util.DataPack;
import com.threerings.util.Log;
import com.threerings.util.Map;
import com.threerings.util.Maps;
import com.threerings.util.MultiLoader;
import com.threerings.util.Set;
import com.threerings.util.StringUtil;
/**
* Like a normal data pack, but we don't deal with any data, and all our filenames are a direct
* translation, so we need no metadata file. We also serve as a tile ImageProvider.
*/
public class ComponentDataPack extends TileDataPack
implements FrameProvider
{
private static var log :Log = Log.getLog(ComponentDataPack);
public function ComponentDataPack (source :Object, actions :Map,
completeListener :Function = null, errorListener :Function = null)
{
super(source, completeListener, errorListener);
_actions = actions;
}
// Documentation inherited from interface
public function getFrames (component :CharacterComponent, action :String,
type :String) :ActionFrames
{
// obtain the action sequence definition for this action
var actseq :ActionSequence = _actions.get(action);
if (actseq == null) {
log.warning("Missing action sequence definition [action=" + action +
", component=" + component + "].");
return null;
}
// determine our image path name
var imgpath :String = action;
var dimgpath :String = ActionSequence.DEFAULT_SEQUENCE;
if (type != null) {
imgpath += "_" + type;
dimgpath += "_" + type;
}
var root :String = component.componentClass.name + "/" + component.name + "/";
var cpath :String = root + imgpath + ".png";
var dpath :String = root + dimgpath + ".png";
// look to see if this tileset is already cached (as the custom action or the default
// action)
var aset :TileSet = _setcache.get(cpath);
if (aset == null) {
aset = _setcache.get(dpath);
if (aset != null) {
// save ourselves a lookup next time
_setcache.put(cpath, aset);
}
}
try {
// then try loading up a tileset customized for this action
if (aset == null) {
if (_zip.getEntry(cpath)) {
aset = TileSet(actseq.tileset.clone());
aset.setImagePath(cpath);
} else if (_zip.getEntry(dpath)) {
actseq = _actions.get(ActionSequence.DEFAULT_SEQUENCE);
aset = TileSet(actseq.tileset.clone());
aset.setImagePath(dpath);
_setcache.put(dpath, aset);
}
}
// if that failed too, we're hosed
if (aset == null) {
// if this is a shadow or crop image, no need to freak out as they are optional
if (!StandardActions.CROP_TYPE == type &&
!StandardActions.SHADOW_TYPE == type) {
log.warning("Unable to locate tileset for action '" + imgpath + "' " +
component + ".");
}
return null;
}
aset.setImageProvider(this);
_setcache.put(cpath, aset);
return new TileSetFrameImage(aset, actseq);
} catch (e :Error) {
log.warning("Error loading tset for action '" + imgpath + "' " + component + ".", e);
}
return null;
}
// Documentation inherited from interface
public function getFramePath (
component :CharacterComponent, action :String, type :String, existentPaths :Set) :String
{
var actionPath :String = makePath(component, action, type);
if (!existentPaths.contains(actionPath)) {
return makePath(component, ActionSequence.DEFAULT_SEQUENCE, type);
}
return actionPath;
}
protected function makePath (component :CharacterComponent, action :String,
type :String) :String
{
var imgpath :String = action;
if (type != null) {
imgpath += "_" + type;
}
var root :String = component.componentClass.name + "/" + component.name + "/";
return root + imgpath + ".png";
}
protected var _actions :Map;
protected var _setcache :Map = Maps.newMapOf(String);
}
}
@@ -0,0 +1,42 @@
//
// $Id$
//
// Nenya library - tools for developing networked games
// Copyright (C) 2002-2012 Three Rings Design, Inc., All Rights Reserved
// http://code.google.com/p/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 {
/** Used to associate a {@link CharacterComponent} with its {@link
* ActionFrames} for a particular action. */
public class ComponentFrames
{
public var ccomp :CharacterComponent;
public var frames :ActionFrames;
public function ComponentFrames (ccomp :CharacterComponent = null,
frames :ActionFrames = null) {
this.ccomp = ccomp;
this.frames = frames;
}
public function toString () :String {
return ccomp + ":" + frames;
}
}
}
@@ -0,0 +1,69 @@
//
// $Id$
//
// Nenya library - tools for developing networked games
// Copyright (C) 2002-2012 Three Rings Design, Inc., All Rights Reserved
// http://code.google.com/p/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 {
/**
* Makes available a collection of character components and associated metadata. Character
* components are animated sequences that can be composited together to create a complete
* character visualization (imagine interchanging pairs of boots, torsos, hats, etc.).
*/
public interface ComponentRepository
{
/**
* Returns the {@link CharacterComponent} object for the given component identifier.
*/
function getComponent (componentId :int) :CharacterComponent;
/**
* Returns the {@link CharacterComponent} object with the given component class and name.
*/
function getComponentByName (className :String, compName :String) :CharacterComponent;
/**
* Returns the {@link ComponentClass} with the specified name or null if none exists with that
* name.
*/
function getComponentClass (className :String) :ComponentClass;
/**
* Iterates over the {@link ComponentClass} instances representing all available character
* component classes.
*/
function getComponentClasses () :Array;
/**
* Iterates over the {@link ActionSequence} instances representing every available action
* sequence.
*/
function getActionSequences () :Array;
/**
* Iterates over the component ids of all components in the specified class.
*/
function getComponentIds (compClass :ComponentClass) :Array;
/**
* Loads up all the specified components and calls notify() when done.
*/
function load (compIds :Array, notify :Function) :void;
}
}
@@ -0,0 +1,199 @@
//
// $Id$
//
// Nenya library - tools for developing networked games
// Copyright (C) 2002-2012 Three Rings Design, Inc., All Rights Reserved
// http://code.google.com/p/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.display.Bitmap;
import flash.display.BitmapData;
import flash.display.DisplayObject;
import flash.geom.Point;
import flash.geom.Rectangle;
import com.threerings.util.Arrays;
import com.threerings.util.Hashable;
import com.threerings.util.Integer;
import com.threerings.util.Map;
import com.threerings.util.Maps;
import com.threerings.util.RandomUtil;
import com.threerings.util.StringUtil;
/**
* An implementation of the {@link ActionFrames} interface that is used
* to lazily create composited character frames when they are requested.
*/
public class CompositedActionFrames
implements ActionFrames, Hashable
{
/**
* Constructs a set of composited action frames with the supplied
* source frames and colorization configuration. The actual component
* frame images will not be composited until they are requested.
*/
public function CompositedActionFrames (frameCache :Map, action :ActionSequence, sources :Array)
{
// sanity check
if (sources == null || sources.length == 0) {
var errmsg :String = "Requested to composite invalid set of source " +
"frames! [action=" + action +
", sources=" + StringUtil.toString(sources) + "].";
throw new Error(errmsg);
}
_frameCache = frameCache;
_sources = sources;
_action = action;
// the sources must all have the same orientation count, so we
// just use the first
_orientCount = _sources[0].frames.getOrientationCount();
}
// documentation inherited from interface
public function getOrientationCount () :int
{
return _orientCount;
}
// documentation inherited from interface
public function getFrames (orient :int, callback :Function) :void
{
var key :CompositedFramesKey = new CompositedFramesKey(this, orient);
var disp :MultiFrameBitmap = _frameCache.get(key);
if (disp == null) {
var listeners :Array = _pending.get(key);
if (listeners != null) {
listeners.push(callback);
} else {
listeners = [callback];
_pending.put(key, listeners);
createFrames(orient, function(disp :MultiFrameBitmap) :void {
_frameCache.put(key, disp);
for each (var func :Function in listeners) {
func(disp.clone());
}
});
}
} else {
callback(disp.clone());
}
}
// documentation inherited from interface
public function getXOrigin (orient :int, frameIdx :int) :int
{
return 0;
}
// documentation inherited from interface
public function getYOrigin (orient :int, frameIdx :int) :int
{
return 0;
}
// documentation inherited from interface
public function cloneColorized (zations :Array) :ActionFrames
{
throw new Error("Unsupported: CompositedActionFrames.cloneColorized()");
}
// documentation inherited from interface
public function cloneTranslated (dx :int, dy :int) :ActionFrames
{
var tsources :Array = new Array(_sources.length);
for (var ii :int = 0; ii < _sources.length; ii++) {
tsources[ii] = new ComponentFrames(
_sources[ii].ccomp, _sources[ii].frames.cloneTranslated(dx, dy));
}
return new CompositedActionFrames(_frameCache, _action, tsources);
}
/**
* Creates our underlying multi-frame image for a particular orientation.
*/
protected function createFrames (orient :int, callback :Function) :void
{
Arrays.stableSort(_sources, function(cf1 :ComponentFrames, cf2 :ComponentFrames) :int {
return (cf1.ccomp.getRenderPriority(_action.name, orient) -
cf2.ccomp.getRenderPriority(_action.name, orient));
});
var idx :int = Arrays.indexOf(_action.orients, orient);
var frameCt :int = _action.tileset.getTileCounts()[idx];
var width :int = _action.tileset.getWidths()[idx];
var height :int = _action.tileset.getHeights()[idx];
var frameBitmaps :Array = new Array(frameCt);
for (var ff :int = 0; ff < frameCt; ff++) {
frameBitmaps[ff] = new Bitmap(new BitmapData(width, height, true, 0x00000000));
}
var region :Rectangle = new Rectangle(0, 0, width, height);
var pt :Point = new Point(0, 0);
compositeFrames(frameBitmaps, _sources, 0, orient, region, pt, frameCt, callback);
}
protected function compositeFrames (frameBitmaps :Array, sources :Array, idx :int, orient :int,
region :Rectangle, pt :Point, frameCt :int, callback :Function) :void
{
if (idx >= sources.length) {
callback(new MultiFrameBitmap(frameBitmaps, _action.framesPerSecond));
return;
}
_sources[idx].frames.getFrames(orient, function (compBitmaps :MultiFrameBitmap) :void {
for (var ii :int = 0; ii < frameCt; ii++) {
frameBitmaps[ii].bitmapData.draw(compBitmaps.getFrame(ii).bitmapData);
}
compositeFrames(frameBitmaps, sources, idx+1, orient, region, pt, frameCt, callback);
});
}
public function equals (other :Object) :Boolean
{
return this === other;
}
public function hashCode () :int
{
return _randHashCode;
}
/** Used to cache our composited action frame images. */
protected var _frameCache :Map;
protected static var _pending :Map = Maps.newMapOf(CompositedFramesKey);
/** The action for which we're compositing frames. */
protected var _action :ActionSequence;
/** The number of orientations. */
protected var _orientCount :int;
/** Our source components and action frames. */
protected var _sources :Array;
/** We keep a hash code to use to identify us nearly-uniquely from the CompositedFramesKey. */
protected var _randHashCode :int = int(Math.random() * Integer.MAX_VALUE);
}
}
@@ -0,0 +1,58 @@
//
// $Id$
//
// Nenya library - tools for developing networked games
// Copyright (C) 2002-2012 Three Rings Design, Inc., All Rights Reserved
// http://code.google.com/p/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 com.threerings.util.Hashable;
/** Used to cache composited frames for a particular action and
* orientation. */
public class CompositedFramesKey
implements Hashable
{
public function CompositedFramesKey (owner :CompositedActionFrames, orient :int) {
_orient = orient;
_owner = owner;
}
public function setOrient (orient :int) :void {
_orient = orient;
}
public function getOwner () :CompositedActionFrames {
return _owner;
}
public function equals (other :Object) :Boolean {
var okey :CompositedFramesKey = CompositedFramesKey(other);
return ((getOwner() == okey.getOwner()) &&
(_orient == okey._orient));
}
public function hashCode () :int {
return _owner.hashCode() ^ _orient;
}
protected var _orient :int;
protected var _owner :CompositedActionFrames;
}
}
@@ -0,0 +1,311 @@
//
// $Id$
//
// Nenya library - tools for developing networked games
// Copyright (C) 2002-2012 Three Rings Design, Inc., All Rights Reserved
// http://code.google.com/p/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.events.Event;
import com.threerings.cast.ComponentDataPack;
import com.threerings.media.tile.TileDataPack;
import com.threerings.util.Arrays;
import com.threerings.util.DataPack;
import com.threerings.util.Log;
import com.threerings.util.Map;
import com.threerings.util.Maps;
import com.threerings.util.StringUtil;
public class DataPackComponentRepository
implements ComponentRepository
{
private static var log :Log = Log.getLog(DataPackComponentRepository);
public function DataPackComponentRepository (compUrl :String)
{
_rootUrl = compUrl;
loadComponentMeta(compUrl + "metadata.jar");
}
protected function loadComponentMeta (compMetaUrl :String) :void
{
_metaPack = new TileDataPack(compMetaUrl, metaLoadingComplete);
}
/**
* Handle the successful completion of datapack loading.
*
* @private
*/
protected function metaLoadingComplete (event :Event) :void
{
var actionXml :XML = _metaPack.getFileAsXML("actions.xml");
var classesXml :XML = _metaPack.getFileAsXML("classes.xml");
for each (var aXml :XML in actionXml.action) {
var action :ActionSequence = ActionSequence.fromXml(aXml);
_actions.put(action.name, action);
}
for each (var cXml :XML in classesXml.elements("class")) {
var compClass :ComponentClass = ComponentClass.fromXml(cXml);
_classes.put(compClass.name, compClass);
}
parseComponentMap(_metaPack.getFileAsString("compmap.txt"));
// Notify them all and clear our list.
for each (var func :Function in _notifyOnLoad) {
func();
}
_notifyOnLoad = [];
}
protected function parseComponentMap (map :String) :void
{
var lines :Array = map.split("\n");
var tileCount :int = int(lines[0]);
for (var ii :int = 1; ii < lines.length; ii++) {
var line :String = lines[ii];
var toks :Array = line.split(" := ");
if (toks.length >= 3) {
var id :int = int(toks[2]);
var cclass :String = String(toks[0]);
var cname :String = String(toks[1]);
createComponent(id, cclass, cname);
}
}
}
/**
* Creates a component and inserts it into the component table.
*/
protected function createComponent (componentId :int, cclass :String, cname :String) :void
{
// look up the component class information
var clazz :ComponentClass = _classes.get(cclass);
if (clazz == null) {
log.warning("Non-existent component class",
"class", cclass, "name", cname, "id", componentId);
return;
}
// create the component - we'll set the frame provider as soon as we can.
var component :CharacterComponent = new CharacterComponent(componentId, cname, clazz, null);
// stick it into the appropriate tables
_components.put(componentId, component);
// we have a hash of lists for mapping components by class/name
var comps :Array = _classComps.get(cclass);
if (comps == null) {
comps = [];
_classComps.put(cclass, comps);
}
if (!Arrays.contains(comps, component)) {
comps.push(component);
} else {
log.info("Requested to register the same component twice?", "comp", component);
}
}
public function notifyOnLoad (func :Function) :void
{
_notifyOnLoad.push(func);
}
/**
* Returns the {@link CharacterComponent} object for the given component identifier.
*/
public function getComponent (componentId :int) :CharacterComponent
{
var component :CharacterComponent = _components.get(componentId);
if (component == null) {
throw new NoSuchComponentError(componentId);
}
return component;
}
/**
* Returns the {@link CharacterComponent} object with the given component class and name.
*/
public function getComponentByName (className :String, compName :String) :CharacterComponent
{
// look up the list for that class
var comps :Array = _classComps.get(className);
if (comps != null) {
for each (var comp :CharacterComponent in comps) {
if (comp.name == compName) {
return comp;
}
}
}
throw new NoSuchComponentError(className + "/" + compName);
}
public function load (compIds :Array, notify :Function) :void
{
var complete :Boolean = true;
var toLoad :Array = [];
for each (var cid :int in compIds) {
var comp :CharacterComponent = getComponent(cid);
if (!comp.isLoaded()) {
toLoad.push(comp);
}
}
var waiting :int = toLoad.length;
if (waiting == 0) {
notify();
} else {
for each (var load :CharacterComponent in toLoad) {
load.notifyOnLoad(function() :void {
waiting--;
if (waiting == 0) {
notify();
}
});
loadComponent(load);
}
}
}
protected function loadComponent (comp :CharacterComponent) :void
{
var packName :String = comp.componentClass.name + "/" + comp.name;
var pack :ComponentDataPack = _packs.get(packName);
if (pack == null || !pack.isComplete()) {
loadPack(packName, comp);
} else {
comp.setFrameProvider(pack);
}
}
protected function loadPack (packName :String, comp :CharacterComponent) :void
{
var pack :ComponentDataPack = _packs.get(packName);
if (pack != null && pack.isComplete()) {
// Already loaded
return;
}
var completeListener :Function = function () :void {
comp.setFrameProvider(_packs.get(packName));
}
var listeners :Array = _packListeners.get(packName);
if (listeners == null) {
listeners = [];
_packListeners.put(packName, listeners);
var packCompleteListener :Function = function () :void {
for each (var listener :Function in listeners) {
listener();
}
_packListeners.remove(packName);
}
pack = new ComponentDataPack(getPackUrl(packName), _actions, packCompleteListener);
_packs.put(packName, pack);
}
listeners.push(completeListener);
}
protected function getPackUrl (packName :String) :String
{
return _rootUrl + packName + "/components.jar";
}
/**
* Returns the {@link ComponentClass} with the specified name or null if none exists with that
* name.
*/
public function getComponentClass (className :String) :ComponentClass
{
return _classes.get(className);
}
/**
* Iterates over the {@link ComponentClass} instances representing all available character
* component classes.
*/
public function getComponentClasses () :Array
{
return _classes.values();
}
/**
* Iterates over the {@link ActionSequence} instances representing every available action
* sequence.
*/
public function getActionSequences () :Array
{
return _actions.values();
}
/**
* Iterates over the component ids of all components in the specified class.
*/
public function getComponentIds (compClass :ComponentClass) :Array
{
var result :Array = [];
_classes.forEach(function (key :int, value :CharacterComponent) :Boolean {
if (value.componentClass.equals(compClass)) {
result.push(key);
}
return false;
});
return result;
}
/** Everyone who cares when we're loaded. */
protected var _notifyOnLoad :Array = [];
/** Contains the component meta-data for all components. */
protected var _metaPack : DataPack;
/** The URL for our components. */
protected var _rootUrl :String;
/** Map of pack name to listeners waiting for that pack to resolve. */
protected var _packListeners :Map = Maps.newMapOf(String);
/** The map of packs we've already loaded up and resolved. */
protected var _packs :Map = Maps.newMapOf(String);
/** All our component classes by name. */
protected var _classes :Map = Maps.newMapOf(String);
/** All our action sequences by name. */
protected var _actions :Map = Maps.newMapOf(String);
/** The map of componentId to CharacterComponent */
protected var _components :Map = Maps.newMapOf(int);
/** A table of component lists indexed on classname. */
protected var _classComps :Map = Maps.newMapOf(String);
}
}
@@ -0,0 +1,50 @@
//
// $Id$
//
// Nenya library - tools for developing networked games
// Copyright (C) 2002-2012 Three Rings Design, Inc., All Rights Reserved
// http://code.google.com/p/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 com.threerings.util.Set;
/**
* Provides a mechanism where by a character component can obtain access
* to its image frames for a particular action in an on demand manner.
*/
public interface FrameProvider
{
/**
* Returns the animation frames (in the eight sprite directions) for
* the specified action of the specified component. May return null if
* the specified action does not exist for the specified component.
*/
function getFrames (
component :CharacterComponent, action :String, type :String) :ActionFrames;
/**
* Returns the file path of the animation frames (in the eight sprite directions) for the
* specified action of the specified component. May return a path to the default action or
* null if the specified action does not exist for the specified component.
*
* @param existentPaths the set of all paths for which there are valid frames.
*/
function getFramePath (
component :CharacterComponent, action :String, type :String, existentPaths :Set):String;
}
}
@@ -0,0 +1,108 @@
//
// $Id$
//
// Nenya library - tools for developing networked games
// Copyright (C) 2002-2012 Three Rings Design, Inc., All Rights Reserved
// http://code.google.com/p/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.display.Bitmap;
import flash.display.Sprite;
import flash.geom.Point;
import flash.events.Event;
import com.threerings.media.Tickable;
import com.threerings.util.StringUtil;
public class MultiFrameBitmap extends Sprite
implements Tickable
{
public function MultiFrameBitmap (frames :Array, fps :Number)
{
_frames = frames;
_bitmap = new Bitmap();
_fps = fps;
_frameRate = fps / 1000.0;
addChild(_bitmap);
setFrame(0);
}
public function tick (tickStamp :int) :void
{
if (_start == 0) {
_start = tickStamp;
}
var elapsedTime :int = (tickStamp - _start);
var frameIndex :int = Math.floor(elapsedTime * _frameRate) % _frames.length;
setFrame(frameIndex);
}
protected function setFrame (index :int) :void
{
if (_curFrameIndex != index) {
_bitmap.bitmapData = Bitmap(_frames[index]).bitmapData;
_curFrameIndex = index;
}
}
public function getFrame (index :int) :Bitmap
{
return _frames[index];
}
public function getFrameCount () :int
{
return _frames.length;
}
public function hitTest (stageX :int, stageY :int) :Boolean
{
if (_curFrameIndex == -1) {
return false;
} else {
if (!_bitmap.hitTestPoint(stageX, stageY, true)) {
// Doesn't even hit the bounds...
return false;
}
// Check the actual pixels...
var pt :Point = _bitmap.globalToLocal(new Point(stageX, stageY));
return _bitmap.bitmapData.hitTest(new Point(0, 0), 0, pt);
}
}
public function clone () :MultiFrameBitmap
{
return new MultiFrameBitmap(_frames, _fps);
}
protected var _frames :Array;
protected var _bitmap :Bitmap;
protected var _start :int;
protected var _frameRate :Number;
protected var _fps :Number;
protected var _curFrameIndex :int = -1;
}
}
@@ -0,0 +1,36 @@
//
// $Id$
//
// Nenya library - tools for developing networked games
// Copyright (C) 2002-2012 Three Rings Design, Inc., All Rights Reserved
// http://code.google.com/p/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 {
/**
* Thrown when an attempt is made to retrieve a non-existent character
* component from the component repository.
*/
public class NoSuchComponentError extends Error
{
public function NoSuchComponentError (comp :*)
{
super((comp is int) ? ("No component with id '" + int(comp) + "'") :
("No component named'" + String(comp) + "'"));
}
}
}
@@ -0,0 +1,138 @@
//
// $Id$
//
// Nenya library - tools for developing networked games
// Copyright (C) 2002-2012 Three Rings Design, Inc., All Rights Reserved
// http://code.google.com/p/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 com.threerings.util.ClassUtil;
import com.threerings.util.Comparable;
import com.threerings.util.DirectionUtil;
import com.threerings.util.Hashable;
import com.threerings.util.Equalable;
import com.threerings.util.Set;
import com.threerings.util.Sets;
import com.threerings.util.StringUtil;
import com.threerings.util.XmlUtil;
/** Used to effect custom render orders for particular actions, orientations, etc. */
public class PriorityOverride
implements Comparable, Hashable, Equalable
{
/** The overridden render priority value. */
public var renderPriority :int;
/** The action, if any, for which this override is appropriate. */
public var action :String = null;
/** The component, if any, for which this override is appropriate. */
public var component :String = null;
/** The orientations, if any, for which this override is appropriate. */
public var orients :Set;
public static function fromXml (xml :XML) :PriorityOverride
{
var override :PriorityOverride = new PriorityOverride;
override.renderPriority = XmlUtil.getIntAttr(xml, "renderPriority");
override.action = XmlUtil.getStringAttr(xml, "action", null);
override.component = XmlUtil.getStringAttr(xml, "component", null);
override.orients = Sets.newSetOf(int);
for each (var orient :int in toOrientArray(
XmlUtil.getStringAttr(xml, "orients", null))) {
override.orients.add(orient);
}
return override;
}
protected static function toOrientArray (str :String) :Array
{
if (str == null || str.length == 0) {
return null;
}
return str.split(",").map(function(element :String, index :int, arr :Array) :int {
return DirectionUtil.fromShortString(StringUtil.trim(element));
});
}
/**
* Determines whether this priority override matches the specified
* action, orientation ant component combination.
*/
public function matches (action :String, component :String, orient :int) :Boolean
{
return (((this.orients == null) || orients.contains(orient)) &&
((this.component == null) || this.component == component) &&
((this.action == null) || this.action == action));
}
// documentation inherited from interface
public function compareTo (po :Object) :int
{
// overrides with both an action and an orientation should come first in the list
var pri :int = priority();
var opri :int = PriorityOverride(po).priority();
if (pri == opri) {
return hashCode() - PriorityOverride(po).hashCode();
} else {
return pri - opri;
}
}
public function hashCode () :int
{
return StringUtil.hashCode(action) ^ StringUtil.hashCode(component) ^ renderPriority;
}
public function equals (other :Object) :Boolean
{
if (!(other is PriorityOverride)) {
return false;
}
var otherOverride :PriorityOverride = PriorityOverride(other);
return otherOverride.renderPriority == renderPriority && otherOverride.action == action &&
otherOverride.component == component;
}
protected function priority () :int
{
var priority :int = 0;
if (component != null) {
priority += 3; // Extra priority to things that are for specific components
}
if (action != null) {
priority++;
}
if (orients != null) {
priority++;
}
return priority;
}
public function toString () :String
{
return "[pri=" + renderPriority + ", action=" + action + ", component=" + component +
", orients=" + orients + "]";
}
}
}
@@ -0,0 +1,43 @@
//
// $Id$
//
// Nenya library - tools for developing networked games
// Copyright (C) 2002-2012 Three Rings Design, Inc., All Rights Reserved
// http://code.google.com/p/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 {
/**
* Actions are referenced by name and this interface defines constants for
* standard actions and action suffixes used by the shadow and cropping
* support.
*/
public class StandardActions
{
/** The name of the standard standing action. */
public static const STANDING :String = "standing";
/** The name of the standard walking action. */
public static const WALKING :String = "walking";
/** A special action sub-type for shadow imagery. */
public static const SHADOW_TYPE :String = "shadow";
/** A special action sub-type for crop imagery. */
public static const CROP_TYPE :String = "crop";
}
}
@@ -0,0 +1,131 @@
//
// $Id$
//
// Nenya library - tools for developing networked games
// Copyright (C) 2002-2012 Three Rings Design, Inc., All Rights Reserved
// http://code.google.com/p/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.display.Bitmap;
import flash.geom.Rectangle;
import com.threerings.media.tile.Tile;
import com.threerings.media.tile.TileSet;
import com.threerings.util.Map;
import com.threerings.util.Maps;
public class TileSetFrameImage
implements ActionFrames
{
public function TileSetFrameImage (set :TileSet, actseq :ActionSequence,
dx :int = 0, dy :int = 0)
{
_set = set;
_actseq = actseq;
_dx = dx;
_dy = dy;
// compute these now to avoid pointless recomputation later
_ocount = actseq.orients.length;
_fcount = set.getTileCount() / _ocount;
// create our mapping from orientation to animation sequence index
for (var ii :int = 0; ii < _ocount; ii++) {
_orients.put(actseq.orients[ii], ii);
}
}
public function getFrames (orient :int, callback :Function) :void
{
var frames :Array = new Array(_fcount);
getFramesFromTiles(frames, 0, orient, callback);
}
protected function getFramesFromTiles (frames :Array, idx :int, orient :int,
callback :Function) :void
{
if (idx >= frames.length) {
callback(new MultiFrameBitmap(frames, _actseq.framesPerSecond));
} else {
var tile :Tile = getTile(orient, idx);
tile.notifyOnLoad(function() :void {
frames[idx] = Bitmap(tile.getImage());
getFramesFromTiles(frames, idx + 1, orient, callback);
});
}
}
// documentation inherited from interface
public function getOrientationCount () :int
{
return _ocount;
}
protected function getTileIndex (orient :int, index :int) :int
{
return _orients.get(orient) * _fcount + index;
}
protected function getTile (orient :int, index :int) :Tile
{
return _set.getTile(getTileIndex(orient, index));
}
// documentation inherited from interface
public function getXOrigin (orient :int, index :int) :int
{
return _actseq.origin.x;
}
// documentation inherited from interface
public function getYOrigin (orient :int, index :int) :int
{
return _actseq.origin.y;
}
// documentation inherited from interface
public function cloneColorized (zations :Array) :ActionFrames
{
return new TileSetFrameImage(_set.cloneWithZations(zations), _actseq);
}
// documentation inherited from interface
public function cloneTranslated (dx :int, dy :int) :ActionFrames
{
return new TileSetFrameImage(_set, _actseq, dx, dy);
}
/** The tileset from which we obtain our frame images. */
protected var _set :TileSet;
/** The action sequence for which we're providing frame images. */
protected var _actseq :ActionSequence;
/** A translation to apply to the images. */
protected var _dx :int;
protected var _dy :int;
/** Frame and orientation counts. */
protected var _fcount :int;
protected var _ocount :int;
/** A mapping from orientation code to animation sequence index. */
protected var _orients :Map = Maps.newMapOf(int);
}
}
@@ -0,0 +1,79 @@
//
// $Id$
//
// Nenya library - tools for developing networked games
// Copyright (C) 2002-2012 Three Rings Design, Inc., All Rights Reserved
// http://code.google.com/p/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.flex {
import mx.controls.List;
import mx.core.EdgeMetrics;
/**
* A list that can be configured with the vertical scrollbar on the left or right side.
*/
public class AmbidextrousList extends List
{
/**
* Get whether the scrollbar is on the left side.
*/
public function get scrollBarOnLeft () :Boolean
{
return _scrollLeft;
}
/**
* Sets whether to place the scrollbar on the left or right side.
*/
public function set scrollBarOnLeft (scrollLeft :Boolean) :void
{
_scrollLeft = scrollLeft;
scrollAreaChanged = true;
invalidateDisplayList();
}
/** @inheritDoc */
override public function get viewMetrics () :EdgeMetrics
{
var em :EdgeMetrics = super.viewMetrics;
if (_scrollLeft && verticalScrollBar && verticalScrollBar.visible) {
em.right -= verticalScrollBar.minWidth;
em.left += verticalScrollBar.minWidth;
}
return em;
}
/** @inheritDoc */
override protected function updateDisplayList (uw :Number, uh :Number) :void
{
super.updateDisplayList(uw, uh);
if (_scrollLeft) {
var vm :EdgeMetrics = viewMetrics;
verticalScrollBar.move(vm.left - verticalScrollBar.minWidth, vm.top);
}
}
/** Do we want to have the scrollbar on the left? */
protected var _scrollLeft :Boolean;
}
}
@@ -0,0 +1,237 @@
//
// $Id$
//
// Nenya library - tools for developing networked games
// Copyright (C) 2002-2012 Three Rings Design, Inc., All Rights Reserved
// http://code.google.com/p/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.flex {
import flash.display.DisplayObjectContainer;
import flash.events.Event;
import flash.events.FocusEvent;
import flash.events.KeyboardEvent;
import flash.events.MouseEvent;
import flash.ui.Keyboard;
import mx.containers.HBox;
import mx.core.Application;
import mx.controls.TextInput;
import mx.events.FlexEvent;
import com.threerings.util.Arrays;
import com.threerings.util.DelayUtil;
import com.threerings.util.StringUtil;
import com.threerings.flex.CommandButton;
import com.threerings.crowd.chat.client.ChatDirector;
import com.threerings.crowd.chat.data.ChatCodes;
import com.threerings.crowd.util.CrowdContext;
/**
* The chat control widget.
*/
public class ChatControl extends HBox
{
/**
* Request focus for the oldest ChatControl.
*/
public static function grabFocus () :void
{
if (_controls.length > 0) {
(_controls[0] as ChatControl).setFocus();
}
}
public function ChatControl (
ctx :CrowdContext, sendButtonLabel :String = "send",
height :Number = NaN, controlHeight :Number = NaN)
{
_ctx = ctx;
_chatDtr = _ctx.getChatDirector();
this.height = height;
styleName = "chatControl";
addChild(_txt = new ChatInput());
_txt.addEventListener(FlexEvent.ENTER, sendChat, false, 0, true);
_txt.addEventListener(KeyboardEvent.KEY_UP, handleKeyUp);
_but = new CommandButton(sendButtonLabel, sendChat);
addChild(_but);
if (!isNaN(controlHeight)) {
_txt.height = controlHeight;
_but.height = controlHeight;
}
addEventListener(Event.ADDED_TO_STAGE, handleAddRemove);
addEventListener(Event.REMOVED_FROM_STAGE, handleAddRemove);
}
/**
* Provides access to the text field we use to accept chat.
*/
public function get chatInput () :ChatInput
{
return _txt;
}
/**
* Provides access to the send button.
*/
public function get sendButton () :CommandButton
{
return _but;
}
override public function set enabled (en :Boolean) :void
{
super.enabled = en;
if (_txt != null) {
_txt.enabled = en;
_but.enabled = en;
}
}
/**
* Request focus to this chat control.
*/
override public function setFocus () :void
{
_txt.setFocus();
}
/**
* Configures the chat director to which we should send our chat. Pass null to restore our
* default chat director.
*/
public function setChatDirector (chatDtr :ChatDirector) :void
{
_chatDtr = (chatDtr == null) ? _ctx.getChatDirector() : chatDtr;
}
/**
* Configures the background color of the text entry area.
*/
public function setChatColor (color :uint) :void
{
_txt.setStyle("backgroundColor", color);
}
/**
* Handles FlexEvent.ENTER and the action from the send button.
*/
protected function sendChat (... ignored) :void
{
var message :String = StringUtil.trim(_txt.text);
if ("" == message) {
return;
}
var result :String = _chatDtr.requestChat(null, message, true);
if (result != ChatCodes.SUCCESS) {
_chatDtr.displayFeedback(null, result);
return;
}
// If there was no error, clear the entry area in prep for the next entry event
DelayUtil.delayFrame(function () : void {
// WORKAROUND
// Originally we cleared the text immediately, but with flex 3.2 this broke
// for *some* people. Weird! We're called from the event dispatcher for the
// enter key, so it's possible that the default action is booching it?
// In any case, this could possibly be removed in the future by the ambitious.
// Note also: Flex's built-in callLater() doesn't work, but DelayUtil does. WTF?!?
_txt.text = "";
});
_histidx = -1;
}
protected function scrollHistory (next :Boolean) :void
{
var size :int = _chatDtr.getCommandHistorySize();
if ((_histidx == -1) || (_histidx == size)) {
_curLine = _txt.text;
_histidx = size;
}
_histidx = (next) ? Math.min(_histidx + 1, size)
: Math.max(_histidx - 1, 0);
var text :String = (_histidx == size) ? _curLine : _chatDtr.getCommandHistory(_histidx);
_txt.text = text;
_txt.setSelection(text.length, text.length);
}
/**
* Handles Event.ADDED_TO_STAGE and Event.REMOVED_FROM_STAGE.
*/
protected function handleAddRemove (event :Event) :void
{
if (event.type == Event.ADDED_TO_STAGE) {
// set up any already-configured text
_txt.text = _curLine;
_histidx = -1;
// request focus
callLater(_txt.setFocus);
_controls.push(this);
} else {
_curLine = _txt.text;
Arrays.removeAll(_controls, this);
}
}
protected function handleKeyUp (event :KeyboardEvent) :void
{
switch (event.keyCode) {
case Keyboard.UP: scrollHistory(false); break;
case Keyboard.DOWN: scrollHistory(true); break;
}
}
/** Our client-side context. */
protected var _ctx :CrowdContext;
/** The director to which we are sending chat requests. */
protected var _chatDtr :ChatDirector;
/** The place where the user may enter chat. */
protected var _txt :ChatInput;
/** The current index in the chat command history. */
protected var _histidx :int = -1;
/** The button for sending chat. */
protected var _but :CommandButton;
/** An array of the currently shown-controls. */
protected static var _controls :Array = [];
/** The preserved current line of text when traversing history or carried between instances of
* ChatControl. */
protected static var _curLine :String;
}
}
@@ -0,0 +1,103 @@
//
// $Id$
//
// Nenya library - tools for developing networked games
// Copyright (C) 2002-2012 Three Rings Design, Inc., All Rights Reserved
// http://code.google.com/p/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.flex {
import flash.display.DisplayObjectContainer;
import flash.events.Event;
import mx.controls.TextArea;
import com.threerings.crowd.chat.client.ChatDirector;
import com.threerings.crowd.chat.client.ChatDisplay;
import com.threerings.crowd.chat.data.ChatMessage;
import com.threerings.crowd.chat.data.UserMessage;
import com.threerings.crowd.util.CrowdContext;
/**
* A very simple chat display.
*/
public class ChatDisplayBox extends TextArea
implements ChatDisplay
{
public function ChatDisplayBox (ctx :CrowdContext)
{
_ctx = ctx;
this.editable = false;
// TODO
width = 400;
height = 150;
addEventListener(Event.ADDED_TO_STAGE, handleAddRemove);
addEventListener(Event.REMOVED_FROM_STAGE, handleAddRemove);
}
// documentation inherited from interface ChatDisplay
public function clear () :void
{
this.htmlText = "";
}
// documentation inherited from interface ChatDisplay
public function displayMessage (msg :ChatMessage) :void
{
if (!_scrollBot) {
_scrollBot = (verticalScrollPosition == maxVerticalScrollPosition);
}
// display the message
if (msg is UserMessage) {
this.htmlText += "<font color=\"red\">&lt;" +
(msg as UserMessage).speaker + "&gt;</font> ";
}
this.htmlText += msg.message;
}
// handle us being added or removed from the stage
protected function handleAddRemove (event :Event) :void
{
var chatdir :ChatDirector = _ctx.getChatDirector();
if (event.type == Event.ADDED_TO_STAGE) {
chatdir.addChatDisplay(this);
} else {
chatdir.removeChatDisplay(this);
}
}
// documentation inherited
override protected function updateDisplayList (uw :Number, uh :Number) :void
{
super.updateDisplayList(uw, uh);
if (_scrollBot) {
verticalScrollPosition = maxVerticalScrollPosition;
_scrollBot = false;
}
}
/** The giver of life. */
protected var _ctx :CrowdContext;
protected var _scrollBot :Boolean;
}
}
@@ -0,0 +1,107 @@
//
// $Id$
//
// Nenya library - tools for developing networked games
// Copyright (C) 2002-2012 Three Rings Design, Inc., All Rights Reserved
// http://code.google.com/p/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.flex {
import flash.events.FocusEvent;
import flash.text.TextField;
import mx.controls.TextInput;
import com.threerings.text.TextFieldUtil;
/**
* The class name of an image to use as the input prompt.
*/
[Style(name="prompt", type="Class")]
/**
* A special TextInput for entering Chat. One of these is used in ChatControl.
*
* A standard TextInput has the stupid behavior of selecting all the text when it receives
* focus. Disable that so that we can receive focus and people can type and we don't blow away
* whatever they had before.
*/
public class ChatInput extends TextInput
{
public function ChatInput ()
{
width = 147;
showPrompt(true);
}
override public function stylesInitialized () :void
{
super.stylesInitialized();
checkShowPrompt();
}
override public function styleChanged (styleProp :String) :void
{
super.styleChanged(styleProp);
if (styleProp == "prompt") {
checkShowPrompt();
}
}
protected function checkShowPrompt () :void
{
showPrompt(focusManager == null || focusManager.getFocus() != this);
}
protected function showPrompt (show :Boolean) :void
{
setStyle("backgroundImage", (show && ("" == text)) ? getStyle("prompt") : undefined);
}
override protected function createChildren () :void
{
super.createChildren();
// For some reason, in embeds, during certain circumstances, it's really hard to focus
// chat. This makes that work better. The problem was *not* testable locally, only
// on embeds in production. (I don't know why.)
TextFieldUtil.setFocusable(TextField(textField));
}
override protected function focusInHandler (event :FocusEvent) :void
{
var oldValue :Boolean = textField.selectable;
textField.selectable = false;
try {
super.focusInHandler(event);
} finally {
textField.selectable = oldValue;
}
showPrompt(false);
}
override protected function focusOutHandler (event :FocusEvent) :void
{
super.focusOutHandler(event);
showPrompt(true);
}
}
}
@@ -0,0 +1,127 @@
//
// $Id$
//
// Nenya library - tools for developing networked games
// Copyright (C) 2002-2012 Three Rings Design, Inc., All Rights Reserved
// http://code.google.com/p/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.flex {
import flash.events.MouseEvent;
import mx.controls.Button;
import com.threerings.util.CommandEvent;
/**
* A command button simply dispatches a Controller command (with an optional
* argument) when it is clicked.
*/
public class CommandButton extends Button
{
/**
* Create a command button.
*
* @param label the label text for the button.
* @param cmdOrFn either a String, which will be the CommandEvent command to dispatch,
* or a function, which will be called when clicked.
* @param arg the argument for the CommentEvent or the function. If the arg is an Array
* then those parameters are used for calling the function.
*/
public function CommandButton (label :String = null, cmdOrFn :* = null, arg :Object = null)
{
validateCmd(cmdOrFn);
this.label = label;
_cmdOrFn = cmdOrFn;
_arg = arg;
buttonMode = true;
}
/**
* Set the command and argument to be issued when this button is pressed.
*/
public function setCommand (cmd :String, arg :Object = null) :void
{
_cmdOrFn = cmd;
_arg = arg;
}
/**
* Set a callback function to call when the button is pressed.
*/
public function setCallback (fn :Function, arg :Object = null) :void
{
_cmdOrFn = fn;
_arg = arg;
}
/**
* Get the argument (or arguments, in an Array).
*/
public function getArg () :Object
{
return _arg;
}
/**
* Emulate a user click, dispatching all relevant events.
*/
public function activate () :void
{
dispatchEvent(new MouseEvent(MouseEvent.CLICK));
}
override public function set enabled (val :Boolean) :void
{
super.enabled = val;
buttonMode = val;
}
override protected function clickHandler (event :MouseEvent) :void
{
super.clickHandler(event);
processCommandClick(this, event, _cmdOrFn, getArg());
}
internal static function validateCmd (cmdOrFn :Object) :void
{
if (cmdOrFn != null && !(cmdOrFn is String) && !(cmdOrFn is Function)) {
// runtime errors suck, but this is actionscript
throw new Error("cmdOrFn must be a String or Function.");
}
}
internal static function processCommandClick (
button :Button, event :MouseEvent, cmdOrFn :Object, arg :Object) :void
{
if (button.enabled) {
event.stopImmediatePropagation();
if (arg == null && button.toggle) {
arg = button.selected;
}
CommandEvent.dispatch(button, cmdOrFn, arg);
}
}
/** The command (String) to submit, or the function (Function) to call
* when clicked, */
protected var _cmdOrFn :Object;
/** The argument that accompanies our command. */
protected var _arg :Object;
}
}
@@ -0,0 +1,85 @@
//
// $Id$
//
// Nenya library - tools for developing networked games
// Copyright (C) 2002-2012 Three Rings Design, Inc., All Rights Reserved
// http://code.google.com/p/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.flex {
import flash.events.MouseEvent;
import mx.controls.CheckBox;
import com.threerings.util.CommandEvent;
/**
* A checkbox that dispatches a command (or calls a function) when it is toggled.
*/
public class CommandCheckBox extends CheckBox
{
/**
* Create a command checkbox.
*
* @param label the label text for the checkbox.
* @param cmdOrFn either a String, which will be the CommandEvent command to dispatch,
* or a function, which will be called when clicked.
* @param arg the argument for the CommentEvent or the function. If the arg is an Array
* then those parameters are used for calling the function.
* Note that if arg is null, the actual argument passed will be the 'selected' state of the
* checkbox. If you really want to call a method with no args, specify arg as an emtpy Array.
*/
public function CommandCheckBox (label :String = null, cmdOrFn :* = null, arg :Object = null)
{
CommandButton.validateCmd(cmdOrFn);
this.label = label;
_cmdOrFn = cmdOrFn;
_arg = arg;
}
/**
* Set the command and argument to be issued when this button is pressed.
*/
public function setCommand (cmd :String, arg :Object = null) :void
{
_cmdOrFn = cmd;
_arg = arg;
}
/**
* Set a callback function to call when the button is pressed.
*/
public function setCallback (fn :Function, arg :Object = null) :void
{
_cmdOrFn = fn;
_arg = arg;
}
override protected function clickHandler (event :MouseEvent) :void
{
super.clickHandler(event);
CommandButton.processCommandClick(this, event, _cmdOrFn, _arg);
}
/** The command (String) to submit, or the function (Function) to call
* when clicked, */
protected var _cmdOrFn :Object;
/** The argument that accompanies our command. */
protected var _arg :Object;
}
}
@@ -0,0 +1,126 @@
//
// $Id$
//
// Nenya library - tools for developing networked games
// Copyright (C) 2002-2012 Three Rings Design, Inc., All Rights Reserved
// http://code.google.com/p/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.flex {
import flash.events.Event;
import mx.controls.ComboBox;
import mx.events.ListEvent;
import com.threerings.util.CommandEvent;
import com.threerings.util.Util;
/**
* A combo box that dispatches a command (or calls a callback) when an item is selected.
* The argument will be the 'data' value of the selected item.
* NOTE: Unlike the other Command* controls, CommandComboBox allows a null cmd/callback
* to be specified.
*/
public class CommandComboBox extends ComboBox
{
/** The field of the selectedItem object used as the 'data'. If this property is null,
* then the item is the data. */
public var dataField :String = "data";
/**
* Create a command combobox.
*
* @param cmdOrFn either a String, which will be the CommandEvent command to dispatch,
* or a function, which will be called when changed.
*/
public function CommandComboBox (cmdOrFn :* = null)
{
CommandButton.validateCmd(cmdOrFn);
_cmdOrFn = cmdOrFn;
addEventListener(ListEvent.CHANGE, handleChange, false, int.MIN_VALUE);
}
/**
* Set the command and argument to be issued when this button is pressed.
*/
public function setCommand (cmd :String) :void
{
_cmdOrFn = cmd;
}
/**
* Set a callback function to call when the button is pressed.
*/
public function setCallback (fn :Function) :void
{
_cmdOrFn = fn;
}
/**
* Set the selectedItem based on the data field.
*/
public function set selectedData (data :Object) :void
{
for (var ii :int = 0; ii < dataProvider.length; ++ii) {
if (Util.equals(data, itemToData(dataProvider[ii]))) {
this.selectedIndex = ii;
return;
}
}
// not found, clear the selection
this.selectedIndex = -1;
}
/**
* The value that will be passed to the command or function based on dataField and the
* current selected item.
*/
public function get selectedData () :Object
{
return itemToData(this.selectedItem);
}
/**
* Extract the data from the specified item. This can be expanded in the future
* if we provide a 'dataFunction'.
*/
protected function itemToData (item :Object) :Object
{
if (item != null && dataField != null) {
try {
return item[dataField];
} catch (re :ReferenceError) {
// fallback to just returning the item
}
}
return item;
}
protected function handleChange (event :ListEvent) :void
{
if (_cmdOrFn != null && this.selectedIndex != -1) {
CommandEvent.dispatch(this, _cmdOrFn, selectedData);
}
}
/** The command (String) to submit, or the function (Function) to call
* when changed, */
protected var _cmdOrFn :Object;
}
}
@@ -0,0 +1,90 @@
//
// $Id$
//
// Nenya library - tools for developing networked games
// Copyright (C) 2002-2012 Three Rings Design, Inc., All Rights Reserved
// http://code.google.com/p/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.flex {
import flash.events.MouseEvent;
import mx.controls.LinkButton;
import com.threerings.util.CommandEvent;
/**
* A CommandLinkButton looks like a link but dispatches a Controller command
* when clicked.
*/
public class CommandLinkButton extends LinkButton
{
/**
* Create a command link button.
*
* @param label the label text for the button.
* @param cmdOrFn either a String, which will be the CommandEvent command to dispatch,
* or a function, which will be called when clicked.
* @param arg the argument for the CommentEvent or the function. If the arg is an Array
* then those parameters are used for calling the function.
*/
public function CommandLinkButton (label :String = null, cmdOrFn :* = null, arg :Object = null)
{
CommandButton.validateCmd(cmdOrFn);
this.label = label;
_cmdOrFn = cmdOrFn;
_arg = arg;
}
override public function set enabled (enable :Boolean) :void
{
super.enabled = enable;
buttonMode = enable;
}
/**
* Set the command and argument to be issued when this button is pressed.
*/
public function setCommand (cmd :String, arg :Object = null) :void
{
_cmdOrFn = cmd;
_arg = arg;
}
/**
* Set a callback function to call when the button is pressed.
*/
public function setCallback (fn :Function, arg :Object = null) :void
{
_cmdOrFn = fn;
_arg = arg;
}
override protected function clickHandler (event :MouseEvent) :void
{
super.clickHandler(event);
CommandButton.processCommandClick(this, event, _cmdOrFn, _arg);
}
/** The command (String) to submit, or the function (Function) to call
* when clicked, */
protected var _cmdOrFn :Object;
/** Any argument that accompanies our command. */
protected var _arg :Object;
}
}
@@ -0,0 +1,537 @@
//
// $Id$
//
// Nenya library - tools for developing networked games
// Copyright (C) 2002-2012 Three Rings Design, Inc., All Rights Reserved
// http://code.google.com/p/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.flex {
import flash.display.DisplayObject;
import flash.display.DisplayObjectContainer;
import flash.events.Event;
import flash.events.IEventDispatcher;
import flash.events.MouseEvent;
import flash.geom.Point;
import flash.geom.Rectangle;
import mx.controls.Button;
import mx.controls.Menu;
import mx.controls.listClasses.BaseListData;
import mx.controls.listClasses.IListItemRenderer;
import mx.controls.menuClasses.IMenuItemRenderer;
import mx.controls.menuClasses.MenuListData;
import mx.core.mx_internal;
import mx.core.Application;
import mx.core.ClassFactory;
import mx.core.IFlexDisplayObject;
import mx.core.ScrollPolicy;
import mx.events.MenuEvent;
import mx.managers.PopUpManager;
import com.threerings.util.CommandEvent;
import com.threerings.flex.PopUpUtil;
import com.threerings.flex.menuClasses.CommandMenuItemRenderer;
import com.threerings.flex.menuClasses.CommandListData;
use namespace mx_internal;
/**
* A pretty standard menu that can submit CommandEvents if menu items have "command" and possibly
* "arg" properties. Commands are submitted to controllers for processing. Alternatively, you may
* specify "callback" properties that specify a function closure to call, with the "arg" property
* containing either a single arg or an array of args.
*
* Another property now supported is "iconObject", which is an already-instantiated
* IFlexDisplayObject to use as the icon. This will only be applied if the "icon" property
* (which specifies a Class) is not used.
*
* Another 'type' now supported is "title", which gives the label for that item the style
* '.menuTitle' and disables it. Your ".menuTitle" style should use the disabledColor.
*
* Example dataProvider array:
* [ { label: "Go home", icon: homeIconClass,
* command: Controller.GO_HOME, arg: homeId },
* { type: "separator"},
{ label: "Crazytown", callback: setCrazy, arg: [ true, false ] },
* { label: "Other places", children: subMenuArray }
* ];
*
* See "Defining menu structure and data" in the Flex manual for the full list.
*
* CommandMenu is scrollable. This can be forced by setting verticalScrollPolicy to
* ScrollPolicy.AUTO or ScrollPolicy.ON and setting the maxHeight. Also, if the scrolling isn't
* forced on, but the menu does not fit within the vertical bounds given (either the stage size by
* default, or the height of the rectangle given in setBounds()), scrolling will be turned on
* so that none of the content is lost.
*
* Note: we don't extend flexlib's ScrollableMenu (or its sub-class ScrollableArrowMenu) because it
* does some weird stuff in measure() that forces some of our menus down to a very small height...
*/
public class CommandMenu extends Menu
{
/**
* Factory method to create a command menu.
*
* @param items an array of menu items.
* @param dispatcher an override event dispatcher to use for command events, rather than
* our parent.
*/
public static function createMenu (
items :Array, dispatcher :IEventDispatcher = null) :CommandMenu
{
var menu :CommandMenu = new CommandMenu();
menu.owner = DisplayObjectContainer(Application.application);
menu.tabEnabled = false;
menu.showRoot = true;
menu.setDispatcher(dispatcher);
Menu.popUpMenu(menu, null, items); // does not actually pop up, but needed.
return menu;
}
/**
* Add a title to the TOP of the specified menu.
*
* @param icon may be a Class or IFlexDisplayObject.
*/
public static function addTitle (
menuItems :Array, title :String, icon :* = null, separatorAfter :Boolean = true) :void
{
var o :Object = { label: title, type: "title" };
if (icon is Class) {
o["icon"] = icon;
} else if (icon is IFlexDisplayObject) {
o["iconObject"] = icon;
}
if (separatorAfter) {
menuItems.unshift({ type: "separator" });
}
menuItems.unshift(o);
}
/**
* Add a separator to the specified menu, unless it would not make sense to do so, because
* the menu is empty or already ends with a separator.
*/
public static function addSeparator (menuItems :Array) :void
{
const len :int = menuItems.length;
if (len > 0 && menuItems[len - 1].type != "separator") {
menuItems.push({ type: "separator" });
}
}
/**
* The mx.controls.Menu class overrides setting and getting the verticalScrollPolicy
* basically setting the verticalScrollPolicy did nothing, and getting it always returned
* ScrollPolicy.OFF. So that's not going to work if we want the menu to scroll. Here we
* reinstate the verticalScrollPolicy setter, and keep a local copy of the value in a
* protected variable _verticalScrollPolicy.
*
* This setter is basically a copy of what ScrollControlBase and ListBase do.
*/
override public function set verticalScrollPolicy (value :String) :void
{
var newPolicy :String = value.toLowerCase();
itemsSizeChanged = true;
if (_verticalScrollPolicy != newPolicy) {
_verticalScrollPolicy = newPolicy;
dispatchEvent(new Event("verticalScrollPolicyChanged"));
}
invalidateDisplayList();
}
override public function get verticalScrollPolicy () :String
{
return _verticalScrollPolicy;
}
public function CommandMenu ()
{
super();
itemRenderer = new ClassFactory(getItemRenderer());
verticalScrollPolicy = ScrollPolicy.OFF;
variableRowHeight = true;
addEventListener(MenuEvent.ITEM_CLICK, itemClicked);
addEventListener(MenuEvent.MENU_HIDE, menuHidden);
}
/**
* Called in the CommandMenu constructor, this should return the item rendering class for this
* CommandMenu.
*/
protected function getItemRenderer () :Class
{
return CommandMenuItemRenderer;
}
/**
* Configure the button that popped-up this menu, which enables two features:
* 1) If the button is clicked again while the menu is up, it won't re-trigger, the menu will
* just close.
* 2) However the menu closes, if the button is a toggle, it will be de-selected.
*/
public function setTriggerButton (trigger :Button) :void
{
_trigger = trigger;
}
/**
* Configures the event dispatcher to be used when dispatching this menu's command events.
* Normally they will be dispatched on our parent (usually the SystemManager or something).
*/
public function setDispatcher (dispatcher :IEventDispatcher) :void
{
_dispatcher = dispatcher;
}
/**
* Sets a Rectangle (in stage coords) that the menu will attempt to keep submenus positioned
* within. If a submenu is too large to fit, it will position it in the lower right corner
* of this Rectangle (or top if popping upwards, or left if popping leftwards). This
* value defaults to the stage bounds.
*/
public function setBounds (fitting :Rectangle) :void
{
_fitting = fitting;
}
/**
* Actually pop up the menu. This can be used instead of show().
*/
public function popUp (
trigger :DisplayObject, popUpwards :Boolean = false, popLeftwards :Boolean = false) :void
{
_upping = popUpwards;
_lefting = popLeftwards;
var r :Rectangle = trigger.getBounds(trigger.stage);
show(_lefting ? r.left : r.right, _upping ? r.top : r.bottom);
}
/**
* Shows the menu at the specified mouse coordinates.
*/
public function popUpAt (
mx :int, my :int, popUpwards :Boolean = false, popLeftwards :Boolean = false) :void
{
_upping = popUpwards;
_lefting = popLeftwards;
show(mx, my);
}
/**
* Shows the menu at the current mouse location.
*/
public function popUpAtMouse (popUpwards :Boolean = false, popLeftwards :Boolean = false) :void
{
_upping = popUpwards;
_lefting = popLeftwards;
show(); // our show, with no args, pops at the mouse
}
/**
* Just like our superclass's show(), except that when invoked with no args, causes the menu to
* show at the current mouse location instead of the top-left corner of the application.
* Also, we ensure that the resulting menu is in-bounds.
*/
override public function show (xShow :Object = null, yShow :Object = null) :void
{
if (xShow == null) {
xShow = DisplayObject(Application.application).mouseX;
}
if (yShow == null) {
yShow = DisplayObject(Application.application).mouseY;
}
super.show(xShow, yShow);
// reposition now that we know our size
if (_lefting) {
y = x - getExplicitOrMeasuredWidth();
}
if (_upping) {
y = y - getExplicitOrMeasuredHeight();
}
var fitting :Rectangle = _fitting || screen;
PopUpUtil.fitInRect(this, fitting);
// if after fitting as best we can, the menu is outside of the declared bounds, we force
// scrolling and set the max height
if (y < fitting.y) {
verticalScrollPolicy = ScrollPolicy.AUTO;
maxHeight = fitting.height;
y = fitting.y;
}
// set up some stuff to clean up and behave
systemManager.topLevelSystemManager.getSandboxRoot().addEventListener(
MouseEvent.MOUSE_DOWN, handleMouseDownOutside, false, 1, true);
}
override public function hide () :void
{
super.hide();
systemManager.topLevelSystemManager.getSandboxRoot().removeEventListener(
MouseEvent.MOUSE_DOWN, handleMouseDownOutside);
}
/**
* The Menu class overrode configureScrollBars() and made the function do nothing. That means
* the scrollbars don't know how to draw themselves, so here we reinstate configureScrollBars.
* This is basically a copy of the same method from the mx.controls.List class.
*/
override protected function configureScrollBars () :void
{
var rowCount :int = listItems.length;
if (rowCount == 0) {
return;
}
// if there is more than one row and it is a partial row we don't count it
if (rowCount > 1 &&
rowInfo[rowCount - 1].y + rowInfo[rowCount - 1].height > listContent.height) {
rowCount--;
}
// offset, when added to rowCount, is hte index of the dataProvider item for that row.
// IOW, row 10 in listItems is showing dataProvider item 10 + verticalScrollPosition -
// lockedRowCount - 1
var offset :int = verticalScrollPosition - lockedRowCount - 1;
// don't count filler rows at the bottom either.
var fillerRows :int = 0;
while (rowCount > 0 && listItems[rowCount - 1].length == 0)
{
if (collection && rowCount + offset >= collection.length) {
rowCount--;
fillerRows++;
} else {
break;
}
}
var colCount :int = listItems[0].length;
var oldHorizontalScrollBar :Object = horizontalScrollBar;
var oldVerticalScrollBar :Object = verticalScrollBar;
var roundedWidth :int = Math.round(unscaledWidth);
var length :int = collection ? collection.length - lockedRowCount : 0;
var numRows :int = rowCount - lockedRowCount;
setScrollBarProperties(Math.round(listContent.width), roundedWidth, length, numRows);
maxVerticalScrollPosition = Math.max(length - numRows, 0);
}
/**
* Callback for MenuEvent.ITEM_CLICK.
*/
protected function itemClicked (event :MenuEvent) :void
{
var arg :Object = getItemProp(event.item, "arg");
var cmdOrFn :Object = getItemProp(event.item, "command");
if (cmdOrFn == null) {
cmdOrFn = getItemProp(event.item, "callback");
}
if (cmdOrFn != null) {
event.stopImmediatePropagation();
CommandEvent.dispatch(_dispatcher == null ? mx_internal::parentDisplayObject :
_dispatcher, cmdOrFn, arg);
}
// else: no warning. There may be non-command menu items mixed in.
}
/**
* Handles MenuEvent.MENU_HIDE.
*/
protected function menuHidden (event :MenuEvent) :void
{
// don't react to submenu hides
if ((event.menu == this) && (_trigger != null) && _trigger.toggle) {
_trigger.selected = false;
}
}
/**
* Handle a down-popping click outside the menu.
*/
protected function handleMouseDownOutside (event :MouseEvent) :void
{
if (event.target == _trigger) {
_trigger.addEventListener(
MouseEvent.CLICK, handleTriggerClick, false, int.MAX_VALUE, true);
}
}
/**
* Suppress the click that wants to land on the trigger button.
*/
protected function handleTriggerClick (event :MouseEvent) :void
{
event.stopImmediatePropagation();
_trigger.removeEventListener(MouseEvent.CLICK, handleTriggerClick);
}
// from ScrollableArrowMenu..
override mx_internal function openSubMenu (row :IListItemRenderer) :void
{
supposedToLoseFocus = true;
var r :Menu = getRootMenu();
var menu :CommandMenu;
// check to see if the menu exists, if not create it
if (!IMenuItemRenderer(row).menu) {
// the only differences between this method and the original method in mx.controls.Menu
// are these few lines.
menu = new CommandMenu();
menu.maxHeight = this.maxHeight;
menu.verticalScrollPolicy = this.verticalScrollPolicy;
menu.variableRowHeight = this.variableRowHeight;
menu.parentMenu = this;
menu.owner = this;
menu.showRoot = showRoot;
menu.dataDescriptor = r.dataDescriptor;
menu.styleName = r;
menu.labelField = r.labelField;
menu.labelFunction = r.labelFunction;
menu.iconField = r.iconField;
menu.iconFunction = r.iconFunction;
menu.itemRenderer = r.itemRenderer;
menu.rowHeight = r.rowHeight;
menu.scaleY = r.scaleY;
menu.scaleX = r.scaleX;
// if there's data and it has children then add the items
if (row.data && _dataDescriptor.isBranch(row.data) &&
_dataDescriptor.hasChildren(row.data)) {
menu.dataProvider = _dataDescriptor.getChildren(row.data);
}
menu.sourceMenuBar = sourceMenuBar;
menu.sourceMenuBarItem = sourceMenuBarItem;
IMenuItemRenderer(row).menu = menu;
PopUpManager.addPopUp(menu, r, false);
}
super.openSubMenu(row);
// if we're lefting, upping or fitting make sure our submenu does so as well
var submenu :Menu = IMenuItemRenderer(row).menu;
if (_lefting) {
submenu.x -= submenu.getExplicitOrMeasuredWidth();
}
if (_upping) {
var rowLoc :Point = row.localToGlobal(new Point());
submenu.y = rowLoc.y - submenu.getExplicitOrMeasuredHeight() + row.height;
}
var fitting :Rectangle = _fitting || screen;
PopUpUtil.fitInRect(submenu, fitting);
// if after fitting as best we can, the menu is outside of the declared bounds, we force
// scrolling and set the max height
if (submenu.y < fitting.y) {
submenu.verticalScrollPolicy = ScrollPolicy.AUTO;
submenu.maxHeight = fitting.height;
submenu.y = fitting.y;
}
}
override protected function measure () :void
{
super.measure();
if (measuredHeight > this.maxHeight) {
measuredHeight = this.maxHeight;
}
if (verticalScrollPolicy == ScrollPolicy.ON || verticalScrollPolicy == ScrollPolicy.AUTO) {
if (verticalScrollBar) {
measuredMinWidth = measuredWidth = measuredWidth + verticalScrollBar.minWidth;
}
}
commitProperties();
}
// from List
override protected function makeListData (data :Object, uid :String, rowNum :int) :BaseListData
{
// Oh, FFS.
// We need to set up these "maxMeasuredIconWidth" fields on the MenuListData, but our
// superclass has made those variables private.
// We can get the values out of another MenuListData, so we just always call super()
// to create one of those, and if we need to make a CommandListData, we construct one
// from the fields in the MenuListData.
var menuListData :MenuListData = super.makeListData(data, uid, rowNum) as MenuListData;
var iconObject :IFlexDisplayObject = getItemProp(data, "iconObject") as IFlexDisplayObject;
if (iconObject != null) {
var cmdListData :CommandListData = new CommandListData(menuListData.label,
menuListData.icon, iconObject, labelField, uid, this, rowNum);
cmdListData.maxMeasuredIconWidth = menuListData.maxMeasuredIconWidth;
cmdListData.maxMeasuredTypeIconWidth = menuListData.maxMeasuredTypeIconWidth;
cmdListData.maxMeasuredBranchIconWidth = menuListData.maxMeasuredBranchIconWidth;
cmdListData.useTwoColumns = menuListData.useTwoColumns;
return cmdListData;
}
return menuListData;
}
/**
* Get the specified property for the specified item, if any. Somewhat similar to bits in the
* DefaultDataDescriptor.
*/
protected function getItemProp (item :Object, prop :String) :Object
{
try {
if (item is XML) {
return String((item as XML).attribute(prop));
} else if (prop in item) {
return item[prop];
}
} catch (e :Error) {
// alas; fall through
}
return null;
}
protected var _dispatcher :IEventDispatcher;
protected var _trigger :Button;
protected var _lefting :Boolean = false;
protected var _upping :Boolean = false;
protected var _fitting :Rectangle = null;
protected var _verticalScrollPolicy :String;
}
}
@@ -0,0 +1,323 @@
//
// $Id$
//
// Nenya library - tools for developing networked games
// Copyright (C) 2002-2012 Three Rings Design, Inc., All Rights Reserved
// http://code.google.com/p/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.flex {
import flash.display.DisplayObject;
import flash.display.InteractiveObject;
import flash.display.Sprite;
import flash.events.MouseEvent;
import flash.text.TextField;
import flash.text.TextFieldType;
import flash.ui.Mouse;
import mx.core.Application;
import mx.managers.SystemManager;
import mx.styles.CSSStyleDeclaration;
import mx.styles.StyleManager;
import com.threerings.util.Map;
import com.threerings.util.Maps;
/**
* A better CursorManager.
*/
public class CursorManager
{
public static const SYSTEM_CURSOR :int = 0;
public static const BUSY_CURSOR :int = 1;
//--------------------------------------------------------------------------
//
// Class methods
//
//--------------------------------------------------------------------------
/**
* Makes the cursor visible.
* Cursor visibility is not reference-counted.
* A single call to the <code>showCursor()</code> method
* always shows the cursor regardless of how many calls
* to the <code>hideCursor()</code> method were made.
*/
public static function showCursor () :void
{
if (_currentCursorId == SYSTEM_CURSOR) {
Mouse.show();
} else {
_cursorHolder.visible = true;
}
}
/**
* Makes the cursor invisible.
* Cursor visibility is not reference-counted.
* A single call to the <code>hideCursor()</code> method
* always hides the cursor regardless of how many calls
* to the <code>showCursor()</code> method were made.
*/
public static function hideCursor () :void
{
_cursorHolder.visible = false;
Mouse.hide();
}
public static function getCurrentCursorId () :int
{
return _currentCursorId;
}
/**
* Creates a new cursor.
*
* @param cursorClass Class of the cursor to display.
*
* @param xOffset Number that specifies the x offset
* of the cursor, in pixels, relative to the mouse pointer.
* The default value is 0.
*
* @param yOffset Number that specifies the y offset
* of the cursor, in pixels, relative to the mouse pointer.
* The default value is 0.
*
* @return The ID of the cursor.
*/
public static function addCursor (
cursorClass :Class, xOffset :int = 0, yOffset :int = 0) :int
{
var cursorId :int = _nextCursorId++;
var rec :CursorRecord = new CursorRecord(cursorClass);
rec.x = xOffset;
rec.y = yOffset;
_cursors.put(cursorId, rec);
return cursorId;
}
/**
* Set the current cursor.
*/
public static function setCursor (id :int) :void
{
if (id == _currentCursorId) {
return;
}
if (!_initialized) {
// oh, let's set it up
_systemManager = Application.application.systemManager;
// set up the busy cursor
var cursorManagerStyleDeclaration :CSSStyleDeclaration =
StyleManager.getStyleDeclaration("CursorManager");
var busyCursorClass :Class =
cursorManagerStyleDeclaration.getStyle("busyCursor");
_busyCursor = new CursorRecord(busyCursorClass);
// The first time a cursor is requested of the CursorManager,
// create a Sprite to hold the cursor symbol
_cursorHolder = new Sprite();
_cursorHolder.mouseEnabled = false;
_systemManager.cursorChildren.addChild(_cursorHolder);
_initialized = true;
}
// figure out what the new cursor will be like
var rec :CursorRecord;
if (id == BUSY_CURSOR) {
rec = _busyCursor;
} else if (id != SYSTEM_CURSOR) {
rec = (_cursors.get(id) as CursorRecord);
if (rec == null) {
// a bogus id was specified
// TODO: throw an error?
return;
}
}
if (rec != null && (rec.cursor is Class)) {
// go ahead and instantiate the class
try {
var disp :DisplayObject = new (rec.cursor as Class);
rec.cursor = disp;
if (disp is InteractiveObject) {
(disp as InteractiveObject).mouseEnabled = false;
}
} catch (err :Error) {
// this cursor is not usable, bail
return;
}
}
// always remove any custom cursors from the hierarchy
if (_cursorHolder.numChildren > 0) {
_cursorHolder.removeChildAt(0);
}
if (id != SYSTEM_CURSOR) {
Mouse.hide();
var currentCursor :DisplayObject = (rec.cursor as DisplayObject);
_cursorHolder.addChild(currentCursor);
_cursorHolder.x = _systemManager.mouseX + rec.x;
_cursorHolder.y = _systemManager.mouseY + rec.y;
_currentXOffset = rec.x;
_currentYOffset = rec.y;
_systemManager.stage.addEventListener(
MouseEvent.MOUSE_MOVE, mouseMoveHandler);
} else {
_currentXOffset = 0;
_currentYOffset = 0;
_systemManager.stage.removeEventListener(
MouseEvent.MOUSE_MOVE, mouseMoveHandler);
Mouse.show();
}
_currentCursorId = id;
}
/**
* Removes a cursor from the cursor list.
* If the cursor being removed is the currently displayed cursor,
* the CursorManager displays the next cursor in the list, if one exists.
* If the list becomes empty, the CursorManager displays
* the default system cursor.
*
* @param cursorID ID of cursor to remove.
*/
public static function removeCursor (id :int) :void
{
var rec :CursorRecord = (_cursors.remove(id) as CursorRecord);
if (rec != null && id == _currentCursorId) {
setCursor(SYSTEM_CURSOR);
}
}
/**
* Removes all of the cursors from the cursor list
* and restores the system cursor.
*/
public static function removeAllCursors () :void
{
_cursors.clear();
if (_currentCursorId != BUSY_CURSOR) {
setCursor(SYSTEM_CURSOR);
}
}
//--------------------------------------------------------------------------
//
// Class event handlers
//
//--------------------------------------------------------------------------
/**
* @private
*/
private static function mouseMoveHandler (event :MouseEvent) :void
{
_cursorHolder.x = _systemManager.mouseX + _currentXOffset;
_cursorHolder.y = _systemManager.mouseY + _currentYOffset;
var target :Object = event.target;
// Do target test.
if (!_overTextField &&
target is TextField && target.type == TextFieldType.INPUT) {
_overTextField = true;
_showSystemCursor = true;
} else if (_overTextField &&
!(target is TextField && target.type == TextFieldType.INPUT)) {
_overTextField = false;
_showCustomCursor = true;
}
// Handle switching between system and custom cursor.
if (_showSystemCursor) {
_showSystemCursor = false;
_cursorHolder.visible = false;
Mouse.show();
}
if (_showCustomCursor) {
_showCustomCursor = false;
_cursorHolder.visible = true;
Mouse.hide();
}
}
/** A mapping of all assigned cursor ids. */
private static const _cursors :Map = Maps.newMapOf(int);
private static var _currentCursorId :int = 0; // SYSTEM_CURSOR
private static var _nextCursorId :int = 2; // skip BUSY
private static var _initialized :Boolean = false;
private static var _cursorHolder :Sprite;
private static var _currentXOffset :int = 0;
private static var _currentYOffset :int = 0;
/** A record for the busy cursor (where it can't be removed). */
private static var _busyCursor :CursorRecord;
private static var _overTextField :Boolean = false;
private static var _overLink :Boolean = false;
private static var _showSystemCursor :Boolean = false;
private static var _showCustomCursor :Boolean = false;
private static var _systemManager :SystemManager = null;
}
}
/**
*/
class CursorRecord extends Object
{
public function CursorRecord (clazz :Class)
{
cursor = clazz;
}
/** The class, or instantiated cursor. */
public var cursor :Object
/** The x/y offset for the hotspot. */
public var x :Number;
public var y :Number;
}
@@ -0,0 +1,205 @@
//
// $Id$
//
// Nenya library - tools for developing networked games
// Copyright (C) 2002-2012 Three Rings Design, Inc., All Rights Reserved
// http://code.google.com/p/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.flex {
import mx.core.IFactory;
import mx.core.ScrollPolicy;
import mx.collections.ArrayCollection;
import mx.collections.IList;
import mx.collections.Sort;
import mx.controls.List;
import com.threerings.util.Util;
import com.threerings.presents.dobj.AttributeChangedEvent;
import com.threerings.presents.dobj.DEvent;
import com.threerings.presents.dobj.DObject;
import com.threerings.presents.dobj.DSet;
import com.threerings.presents.dobj.DSet_Entry;
import com.threerings.presents.dobj.EntryAddedEvent;
import com.threerings.presents.dobj.EntryUpdatedEvent;
import com.threerings.presents.dobj.EntryRemovedEvent;
import com.threerings.presents.dobj.EventListener;
import com.threerings.presents.dobj.NamedEvent;
/**
* A list that renders the contents of a DSet.
*/
public class DSetList extends List
implements EventListener
{
public function DSetList (
renderer :IFactory, sortFn :Function = null, filterFn :Function = null)
{
horizontalScrollPolicy = ScrollPolicy.OFF;
verticalScrollPolicy = ScrollPolicy.ON;
selectable = false;
itemRenderer = renderer;
_data = new ArrayCollection();
var sort :Sort = new Sort();
sort.compareFunction = sortFn;
_data.sort = sort;
_data.filterFunction = filterFn;
_data.refresh();
dataProvider = _data;
}
/**
* Set the data to be displayed by this List, if not being attached to a DObject.
*/
public function setData (array :Array /* of DSet_Entry */) :void
{
_data.source = (array == null) ? [] : array;
refresh();
}
/**
* Refresh the data, which can be called if something external has changed
* the sort order or the rendering.
*/
public function refresh () :void
{
_data.refresh();
}
/**
* Scroll so that the entry with the specified key is visible.
*/
public function scrollToKey (key :Object) :Boolean
{
var scrollIdx :int = findKeyIndex(key, false);
return (scrollIdx == -1) ? false : scrollToIndex(scrollIdx);
}
/**
* Initialize listening on the specified object.
*
* @param object the DObject on which to listen.
* @param field the name of the DSet field that we're displaying.
* @param refreshFields additional fields that cause refresh() to be called should
* AttributeChangedEvents arrive on them.
*/
public function init (object :DObject, field :String, ... refreshFields) :void
{
shutdown();
_object = object;
_field = field;
_refreshFields = refreshFields;
_object.addListener(this);
setEntries(_object[_field] as DSet);
}
/**
* Shut down listening.
*/
public function shutdown () :void
{
if (_object != null) {
_object.removeListener(this);
_object = null;
_field = null;
_refreshFields = null;
setData(null);
}
}
/**
* Safely set the entries, even if null.
*/
protected function setEntries (entries :DSet) :void
{
setData((entries == null) ? null : entries.toArray());
}
/**
* From com.threerings.presents.dobj.EventListener. Not actually part of the public API.
*/
public function eventReceived (event :DEvent) :void
{
if (!(event is NamedEvent)) {
return;
}
var name :String = NamedEvent(event).getName();
if (name == _field) {
if (event is EntryAddedEvent) {
_data.list.addItem(EntryAddedEvent(event).getEntry());
} else if (event is EntryUpdatedEvent) {
var entry :DSet_Entry = EntryUpdatedEvent(event).getEntry();
var upIdx :int = findKeyIndex(entry.getKey());
if (upIdx != -1) {
_data.list.setItemAt(entry, upIdx);
}
} else if (event is EntryRemovedEvent) {
var remIdx :int = findKeyIndex(EntryRemovedEvent(event).getKey());
if (remIdx != -1) {
_data.list.removeItemAt(remIdx);
}
} else if (event is AttributeChangedEvent) {
setEntries(AttributeChangedEvent(event).getValue() as DSet);
return; // skip the full refresh
}
} else if (-1 == _refreshFields.indexOf(name)) {
return; // this name is not our concern: do nothing
}
// we reach this line if the event was for a _refreshField, or a Set event on _field
refresh();
}
/**
* Find the index of the specified entry key from the raw or filtered list.
*/
protected function findKeyIndex (key :Object, raw :Boolean = true) :int
{
var list :IList = raw ? _data.list : _data;
for (var ii :int = 0; ii < list.length; ii++) {
if (Util.equals(key, DSet_Entry(list.getItemAt(ii)).getKey())) {
return ii;
}
}
return -1;
}
/** The collection for the List, This is the filtered/sorted view. For the
* the raw list, we access _data.list. */
protected var _data :ArrayCollection;
/** The object on which we're listening. */
protected var _object :DObject;
/** The fieldname of the DSet in the _object. */
protected var _field :String;
/** Fields on which to watch for updates and call refresh(). */
protected var _refreshFields :Array;
}
}
@@ -0,0 +1,166 @@
//
// $Id$
//
// Nenya library - tools for developing networked games
// Copyright (C) 2002-2012 Three Rings Design, Inc., All Rights Reserved
// http://code.google.com/p/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.flex {
import flash.display.DisplayObject;
import mx.controls.Label;
import mx.controls.Spacer;
import mx.controls.Text;
import mx.core.Container;
import mx.core.UIComponent;
import mx.containers.HBox;
import mx.containers.VBox;
/**
* Flex-related utility methods.
*/
public class FlexUtil
{
/**
* Creates a label with the supplied text, and optionally applies a style class and tooltip
* to it.
*/
public static function createLabel (
text :String, style :String = null, toolTip :String = null) :Label
{
var label :Label = new Label();
label.text = text;
label.styleName = style;
label.toolTip = toolTip;
return label;
}
/**
* Create an uneditable/unselectable multiline Text widget with the specified text and width.
*/
public static function createText (text :String, width :int, style :String = null) :Text
{
var t :Text = new Text();
t.styleName = style;
t.width = width;
t.selectable = false;
t.text = text;
return t;
}
/**
* Create an uneditable/unselectable multiline Text widget with 100% width.
*/
public static function createWideText (text :String, style :String = null) :Text
{
var t :Text = new Text();
t.styleName = style;
t.percentWidth = 100;
t.selectable = false;
t.text = text;
return t;
}
/**
* How hard would it have been for them make Spacer accept two optional arguments?
*/
public static function createSpacer (width :int = 0, height :int = 0) :Spacer
{
var spacer :Spacer = new Spacer();
spacer.width = width;
spacer.height = height;
return spacer;
}
/**
* Return a count of how many visible (includeInLayout=true) children
* are in the specified container.
*/
public static function countLayoutChildren (container :Container) :int
{
var layoutChildren :int = container.numChildren;
for each (var child :Object in container.getChildren()) {
if ((child is UIComponent) && !UIComponent(child).includeInLayout) {
layoutChildren--;
}
}
return layoutChildren;
}
/**
* In flex the 'visible' property controls visibility separate from whether
* the component takes up space in the layout, which is controlled by 'includeInLayout'.
* We usually want to set them together, so this does that for us.
* @return the new visible setting.
*/
public static function setVisible (component :UIComponent, visible :Boolean) :Boolean
{
component.visible = visible;
component.includeInLayout = visible;
return visible;
}
/**
* Creates a new container of the given class and adds the given children to it.
*/
public static function createContainer (clazz :Class, ...children) :Container
{
var container :Container = new clazz() as Container;
for each (var comp :UIComponent in children) {
container.addChild(comp);
}
return container;
}
/**
* Creates a new HBox container and adds the given children to it.
*/
public static function createHBox (...children) :HBox
{
children.unshift(HBox);
return createContainer.apply(null, children) as HBox;
}
/**
* Creates a new VBox container and adds the given children to it.
*/
public static function createVBox (...children) :VBox
{
children.unshift(VBox);
return createContainer.apply(null, children) as VBox;
}
/**
* Creates a simple UIComponent that wraps a display object.
*/
public static function wrap (obj :DisplayObject) :FlexWrapper
{
return new FlexWrapper(obj);
}
/**
* Creates a simple UIComponent that wraps a display object and inherits its size.
*/
public static function wrapSized (obj :DisplayObject) :FlexWrapper
{
return new FlexWrapper(obj, true);
}
}
}
@@ -0,0 +1,44 @@
//
// $Id$
//
// Nenya library - tools for developing networked games
// Copyright (C) 2002-2012 Three Rings Design, Inc., All Rights Reserved
// http://code.google.com/p/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.flex {
import flash.display.DisplayObject;
import mx.core.UIComponent;
/**
* Wraps a non-Flex component for use in Flex.
*/
public class FlexWrapper extends UIComponent
{
public function FlexWrapper (object :DisplayObject, inheritSize :Boolean = false)
{
// don't capture mouse events in this wrapper
mouseEnabled = false;
addChild(object);
if (inheritSize) {
width = object.width;
height = object.height;
}
}
}
}
@@ -0,0 +1,52 @@
//
// $Id$
//
// Nenya library - tools for developing networked games
// Copyright (C) 2002-2012 Three Rings Design, Inc., All Rights Reserved
// http://code.google.com/p/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.flex {
import mx.effects.IEffectInstance;
import mx.effects.Effect;
public class FunctionEffect extends Effect
{
/** The function to call. */
public var func :Function;
/** The arguments to pass to the function. */
public var args :Array;
public function FunctionEffect (target :Object = null)
{
super(target);
instanceClass = FunctionEffectInstance;
}
// documentation inherited
override protected function initInstance (instance :IEffectInstance) :void
{
super.initInstance(instance);
var fe :FunctionEffectInstance = (instance as FunctionEffectInstance);
fe.func = func;
fe.args = args;
}
}
}
@@ -0,0 +1,50 @@
//
// $Id$
//
// Nenya library - tools for developing networked games
// Copyright (C) 2002-2012 Three Rings Design, Inc., All Rights Reserved
// http://code.google.com/p/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.flex {
import mx.effects.effectClasses.ActionEffectInstance;
public class FunctionEffectInstance extends ActionEffectInstance
{
public function FunctionEffectInstance (target :Object)
{
super(target);
}
/** The function to call. */
public var func :Function;
/** The args to pass. */
public var args :Array;
override public function play () :void
{
super.play();
// call the function!
func.apply(null, args);
// and we're done
finishRepeat();
}
}
}
@@ -0,0 +1,117 @@
//
// $Id$
//
// Nenya library - tools for developing networked games
// Copyright (C) 2002-2012 Three Rings Design, Inc., All Rights Reserved
// http://code.google.com/p/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.flex {
import mx.containers.Grid;
import mx.containers.GridItem;
import mx.containers.GridRow;
import mx.controls.Label;
import mx.core.UIComponent;
/**
* Convenience methods for adding children to Grids.
*/
public class GridUtil
{
/**
* Add a new row to the grid, containing the specified
* components.
*
* @param specs a list of components or Strings, or you can follow
* any component with a two-dimensional array that specifies
* grid width/height.
*
* Example: addRow(grid, "labeltxt", _entryField, _bigThing, [2, 2], _smallThing);
*
* All will be put in the same row, but bigThing will have
* colspan=2 rowspan=2
*/
public static function addRow (grid :Grid, ... specs) :GridRow
{
var row :GridRow = new GridRow();
var lastItem :GridItem;
for each (var o :Object in specs) {
if (o is String) {
var lbl :Label = new Label();
lbl.text = String(o);
o = lbl;
}
if (o is UIComponent) {
lastItem = addToRow(row, UIComponent(o));
} else if (o is Array) {
var arr :Array = (o as Array);
lastItem.colSpan = int(arr[0]);
lastItem.rowSpan = int(arr[1]);
} else {
throw new ArgumentError();
}
}
grid.addChild(row);
return row;
}
/**
* Add a child to the specified grid row, returning the
* GridItem created for containment of the child.
*/
public static function addToRow (row :GridRow, comp :UIComponent) :GridItem
{
var item :GridItem = new GridItem();
item.addChild(comp);
row.addChild(item);
return item;
}
/**
* Get the number of cells in a grid.
*/
public static function getCellCount (grid :Grid) :int
{
var count :int = 0;
for (var ii :int = 0; ii < grid.numChildren; ii++) {
count += (grid.getChildAt(ii) as GridRow).numChildren;
}
return count;
}
/**
* Return the contents of the specified cell. Not super fast, as it
* checks each row in order.
*/
public static function getCellAt (grid :Grid, index :int) :UIComponent
{
for (var ii :int = 0; ii < grid.numChildren; ii++) {
var row :GridRow = grid.getChildAt(ii) as GridRow;
if (index < row.numChildren) {
return (row.getChildAt(index) as GridItem).getChildAt(0) as UIComponent;
} else {
index -= row.numChildren; // and then on to the next row
}
}
return null; // never found it. throw an out-of-range error?
}
}
}
@@ -0,0 +1,83 @@
//
// $Id$
//
// Nenya library - tools for developing networked games
// Copyright (C) 2002-2012 Three Rings Design, Inc., All Rights Reserved
// http://code.google.com/p/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.flex {
import flash.display.Stage;
import flash.events.KeyboardEvent;
import flash.events.MouseEvent;
import flash.ui.Keyboard;
import flash.utils.Dictionary;
import mx.core.Application;
import mx.controls.Button;
public class KeyboardManager
{
public static function setShortcut (trigger :*, keyCode :uint, ... modifiers) :void
{
_triggers[trigger] = [ keyCode, modifiers ];
ensureListening();
}
public static function clearShortcut (trigger :*) :void
{
delete _triggers[trigger];
}
protected static function ensureListening () :void
{
var stage :Stage = Application(Application.application).stage;
stage.addEventListener(KeyboardEvent.KEY_DOWN, handleKeyDown);
}
protected static function handleKeyDown (event :KeyboardEvent) :void
{
for (var trigger :* in _triggers) {
var info :Array = _triggers[trigger] as Array;
var keyCode :uint = info[0];
var modifiers :Array = info[1] as Array;
if (event.keyCode == keyCode &&
// (event.altKey == (-1 != modifierCodes.indexOf(Keyboard.ALTERNATE))) &&
(event.ctrlKey == (-1 != modifiers.indexOf(Keyboard.CONTROL))) &&
(event.shiftKey == (-1 != modifiers.indexOf(Keyboard.SHIFT)))) {
wasTriggered(trigger);
}
}
}
protected static function wasTriggered (trigger :*) :void
{
if (trigger is Button) {
var button :Button = Button(trigger);
if (button.stage != null && button.enabled) {
button.dispatchEvent(new MouseEvent(MouseEvent.CLICK));
}
}
}
protected static const _triggers :Dictionary = new Dictionary(true);
}
}
@@ -0,0 +1,75 @@
//
// $Id$
//
// Nenya library - tools for developing networked games
// Copyright (C) 2002-2012 Three Rings Design, Inc., All Rights Reserved
// http://code.google.com/p/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.flex {
import mx.core.ScrollPolicy;
import mx.containers.HBox;
import mx.controls.Label;
import mx.controls.sliderClasses.Slider;
import mx.events.SliderEvent;
/**
* A simple component that displays a label to the left of a slider.
*/
public class LabeledSlider extends HBox
{
/** The actual slider. */
public var slider :Slider;
/**
* Create a LabeledSlider holding the specified slider.
*/
public function LabeledSlider (slider :Slider, labelWidth :int = 17)
{
_label = new Label();
_label.text = String(slider.value);
_label.width = labelWidth;
horizontalScrollPolicy = ScrollPolicy.OFF;
setStyle("horizontalGap", 2);
addChild(_label);
this.slider = slider;
slider.showDataTip = false; // because we do it...
addChild(slider);
slider.addEventListener(SliderEvent.CHANGE, handleSliderChange, false, 0, true);
}
override public function set width (width :Number) :void
{
super.width = width;
slider.width = width - _label.width - 2;
}
protected function handleSliderChange (event :SliderEvent) :void
{
_label.text = String(event.value);
}
protected var _label :Label;
}
}
@@ -0,0 +1,58 @@
//
// $Id$
//
// Nenya library - tools for developing networked games
// Copyright (C) 2002-2012 Three Rings Design, Inc., All Rights Reserved
// http://code.google.com/p/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.flex {
import flash.display.DisplayObjectContainer;
import mx.core.ContainerCreationPolicy;
import mx.core.UIComponent;
import mx.containers.VBox;
public class LazyContainer extends VBox
{
/**
* Create a lazy container that will not create its children
* until needed.
*
* @param creation a function that takes no args and returns
* a UIComponent to be added to the container.
*/
public function LazyContainer (creation :Function)
{
_creation = creation;
creationPolicy = ContainerCreationPolicy.NONE;
}
override public function createComponentsFromDescriptors (
recurse :Boolean = true) :void
{
super.createComponentsFromDescriptors(recurse);
if (_creation != null) {
addChild(_creation() as UIComponent);
_creation = null; // assist gc
}
}
protected var _creation :Function;
}
}
@@ -0,0 +1,63 @@
//
// $Id$
//
// Nenya library - tools for developing networked games
// Copyright (C) 2002-2012 Three Rings Design, Inc., All Rights Reserved
// http://code.google.com/p/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.flex {
import mx.core.ContainerCreationPolicy;
import mx.core.UIComponent;
import mx.containers.TabNavigator;
import mx.containers.VBox;
/**
* A tab navigator that is specially set up for lazy-creating the
* content in each tab.
*/
public class LazyTabNavigator extends TabNavigator
{
public function LazyTabNavigator ()
{
super();
}
/**
* Add a tab to the container. The creation function takes no args and
* returns a UIComponent.
*/
public function addTab (label :String, creation :Function) :void
{
addTabAt(label, creation, numChildren);
}
/**
* Add a tab to the container at the specified index.
* The creation function takes no args and returns a UIComponent.
*/
public function addTabAt (
label :String, creation :Function, index :int) :void
{
var box :LazyContainer = new LazyContainer(creation);
box.label = label;
addChildAt(box, index);
}
}
}
@@ -0,0 +1,134 @@
//
// $Id$
//
// Nenya library - tools for developing networked games
// Copyright (C) 2002-2012 Three Rings Design, Inc., All Rights Reserved
// http://code.google.com/p/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.flex {
import flash.display.Loader;
import flash.display.Sprite;
import mx.core.IFlexAsset;
import mx.core.IFlexDisplayObject;
import mx.core.IInvalidating;
import com.threerings.util.MultiLoader;
/**
* An asset that can be used in Flex, that's loaded via a Loader.
*/
public class LoadedAsset extends Sprite
implements IFlexAsset, IFlexDisplayObject
{
/**
* @param source a String (url) or URLRequest, or Class or a ByteArray.
*/
public function LoadedAsset (source :Object) :void
{
MultiLoader.getLoaders(source, handleLoaded);
}
override public function get height () :Number
{
return (_loader == null) ? _measHeight : super.height;
}
override public function set height (value :Number) :void
{
if (_loader == null) {
_reqHeight = value;
} else {
_loader.height = value;
}
}
override public function get width () :Number
{
return (_loader == null) ? _measWidth : super.width;
}
override public function set width (value :Number) :void
{
if (_loader == null) {
_reqWidth = value;
} else {
_loader.width = value;
}
}
// from IFlexDisplayObject
public function get measuredHeight () :Number
{
return _measHeight;
}
// from IFlexDisplayObject
public function get measuredWidth () :Number
{
return _measWidth;
}
// from IFlexDisplayObject
public function move (x :Number, y :Number) :void
{
this.x = x;
this.y = y;
}
// from IFlexDisplayObject
public function setActualSize (newWidth :Number, newHeight :Number) :void
{
width = newWidth;
height = newHeight;
}
protected function handleLoaded (loader :Loader) :void
{
_loader = loader;
addChild(_loader);
// Note our measured size
_measWidth = loader.width;
_measHeight = loader.height;
// set any size that was set while we were being loaded
if (!isNaN(_reqWidth)) {
loader.width = _reqWidth;
}
if (!isNaN(_reqHeight)) {
loader.height = _reqHeight;
}
// if we have a parent that knows about sizes, tell it we now know our size
if (parent is IInvalidating) {
IInvalidating(parent).invalidateSize();
}
}
protected var _loader :Loader;
// initial / base sizes
protected var _measWidth :Number = 0;
protected var _measHeight :Number = 0;
// requested sizes, during loading
protected var _reqWidth :Number;
protected var _reqHeight :Number;
}
}
@@ -0,0 +1,122 @@
//
// $Id$
//
// Nenya library - tools for developing networked games
// Copyright (C) 2002-2012 Three Rings Design, Inc., All Rights Reserved
// http://code.google.com/p/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.flex {
import flash.geom.Point;
import flash.geom.Rectangle;
import mx.core.UIComponent;
import com.threerings.display.DisplayUtil;
/**
* Flex popup utilities.
*/
public class PopUpUtil
{
/**
* Center the popup inside the stage, to which it should already be added.
*/
public static function center (popup :UIComponent) :void
{
centerInRect(popup, createStageRect(popup));
}
/**
* Center the popup within the specified rectangle.
*/
public static function centerInRect (popup :UIComponent, rect :Rectangle) :void
{
var p :Point = DisplayUtil.centerRectInRect(new Rectangle(0, 0,
popup.getExplicitOrMeasuredWidth(), popup.getExplicitOrMeasuredHeight()), rect);
popup.x = p.x;
popup.y = p.y;
}
/**
* Fit the popup inside the stage.
*/
public static function fit (popup :UIComponent) :void
{
fitInRect(popup, createStageRect(popup));
}
/**
* Fit the popup inside the specified rectangle.
*/
public static function fitInRect (popup :UIComponent, rect :Rectangle) :void
{
var p :Point = DisplayUtil.fitRectInRect(createPopupRect(popup), rect);
popup.x = p.x;
popup.y = p.y;
}
/**
* Try to fit inside the specified rectangle, also avoiding other popups.
*
* @param popup the popup we'll try to move.
* @param bounds the boundary within which to place the popup.
* @param padding extra padding to place between popups (but not around the inside of
* the bounds, do that yourself if you want it).
*/
public static function avoidOtherPopups (
popup :UIComponent, bounds :Rectangle, padding :int = 0) :void
{
var avoid :Array = [];
var r :Rectangle;
// find the other popups we need to avoid
for (var ii :int = popup.parent.numChildren - 1; ii >= 0; ii--) {
var comp :UIComponent = popup.parent.getChildAt(ii) as UIComponent;
if (comp != null && comp.isPopUp && comp.visible && comp != popup) {
r = createPopupRect(comp);
r.inflate(padding, padding);
avoid.push(r);
}
}
r = createPopupRect(popup);
// put the rectangle in-bounds, so that even if avoiding the others fails, we're
// at least in-bounds when fitRectInRect resets to the original position.
var p :Point = DisplayUtil.fitRectInRect(r, bounds);
r.x = p.x;
r.y = p.y; // assigning to topLeft adjusts width/height too, so don't do it!
DisplayUtil.positionRect(r, bounds, avoid);
popup.x = r.x;
popup.y = r.y;
}
/**
* Wee utility method.
*/
protected static function createStageRect (popup :UIComponent) :Rectangle
{
return new Rectangle(0, 0, popup.stage.stageWidth, popup.stage.stageHeight);
}
protected static function createPopupRect (popup :UIComponent) :Rectangle
{
return new Rectangle(popup.x, popup.y,
popup.getExplicitOrMeasuredWidth(), popup.getExplicitOrMeasuredHeight());
}
}
}
@@ -0,0 +1,181 @@
//
// $Id$
//
// Nenya library - tools for developing networked games
// Copyright (C) 2002-2012 Three Rings Design, Inc., All Rights Reserved
// http://code.google.com/p/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.flex {
import flash.display.DisplayObject;
import flash.display.Graphics;
import flash.display.Shape;
import flash.display.Sprite;
import flash.events.Event;
import flash.events.MouseEvent;
import flash.geom.Matrix;
import flash.geom.Rectangle;
import mx.containers.Canvas;
/**
* A control for scrolling some component separately.
*/
public class ScrollBox extends Canvas
{
public function ScrollBox (
target :DisplayObject, maxWidth :int, maxHeight :int)
{
_target = target;
_maxWidth = maxWidth;
_maxHeight = maxHeight;
opaqueBackground = 0xFFFFFF;
_mask = new Shape();
rawChildren.addChild(_mask);
this.mask = _mask;
_box = new Sprite();
rawChildren.addChild(_box);
recheckBounds();
// TODO: only when something changes
addEventListener(Event.ENTER_FRAME, enterFrame);
_box.addEventListener(MouseEvent.MOUSE_DOWN, spritePressed);
}
override public function setActualSize (w :Number, h :Number) :void
{
super.setActualSize(w, h);
recheckBounds();
graphics.clear();
graphics.lineStyle(1, 0x000000, 1.0);
graphics.drawRect(0, 0, width, height);
var g :Graphics = _mask.graphics;
g.clear();
g.beginFill(0xffffff, 0.0);
g.drawRect(0, 0, width, height);
g.endFill();
}
protected function enterFrame (vent :Event) :void
{
if (!_dragging) {
recheckBounds();
}
}
/**
* Get the bounds of the scrollable area. Broken-out for easy overridding.
*/
protected function getScrollBounds () :Rectangle
{
var bounds :Rectangle = _target.transform.pixelBounds;
// var m :Matrix = _target.transform.concatenatedMatrix;
// bounds.topLeft = m.transformPoint(bounds.topLeft);
// bounds.bottomRight = m.transformPoint(bounds.bottomRight);
return bounds;
}
protected function recheckBounds () :void
{
var bounds :Rectangle = getScrollBounds();
// see what's visible, what's not..
var scroller :Rectangle = _target.scrollRect;
if (scroller == null) {
scroller = bounds.clone();
}
if ((_bounds != null) && _bounds.equals(bounds) &&
(_scroller != null) && _scroller.equals(scroller)) {
return;
}
_bounds = bounds.clone();
_scroller = scroller;
_scale = Math.min(1, Math.min(_maxWidth / _bounds.width, _maxHeight / _bounds.height));
width = _scale * _bounds.width;
height = _scale * _bounds.height;
drawBox(_box, _scroller.width * _scale, _scroller.height * _scale);
_box.x = (_scroller.x - _bounds.x) * _scale;
_box.y = (_scroller.y - _bounds.y) * _scale;
}
protected function drawBox (box :Sprite, ww :Number, hh :Number) :void
{
var g :Graphics = _box.graphics;
g.clear();
g.beginFill(0xffffff, 0.3);
g.lineStyle(1, 0x000000, 0.3);
g.drawRect(0, 0, ww, hh);
g.endFill();
}
protected function spritePressed (evt :MouseEvent) :void
{
_box.addEventListener(Event.ENTER_FRAME, boxUpdate);
_box.startDrag(true,
new Rectangle(0, 0, width - _box.width, height - _box.height));
stage.addEventListener(MouseEvent.MOUSE_UP, spriteReleased);
_dragging = true;
}
protected function spriteReleased (evt :MouseEvent) :void
{
stage.removeEventListener(MouseEvent.MOUSE_UP, spriteReleased);
_box.stopDrag();
_box.removeEventListener(Event.ENTER_FRAME, boxUpdate);
_dragging = false;
}
protected function boxUpdate (evt :Event) :void
{
var r :Rectangle = _target.scrollRect;
r.x = (_box.x / _scale) + _bounds.x;
r.y = (_box.y / _scale) + _bounds.y;
_target.scrollRect = r;
}
protected var _dragging :Boolean;
protected var _target :DisplayObject;
protected var _maxWidth :int;
protected var _maxHeight :int;
protected var _box :Sprite;
protected var _mask :Shape;
protected var _scale :Number;
protected var _bounds :Rectangle;
protected var _scroller :Rectangle;
}
}
@@ -0,0 +1,42 @@
//
// $Id$
//
// Nenya library - tools for developing networked games
// Copyright (C) 2002-2012 Three Rings Design, Inc., All Rights Reserved
// http://code.google.com/p/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.flex.menuClasses {
import mx.core.IFlexDisplayObject;
import mx.core.IUIComponent;
import mx.controls.menuClasses.MenuListData;
public class CommandListData extends MenuListData
{
/** An already-instantiated icon, to use if the icon property is null. */
public var iconObject :IFlexDisplayObject;
public function CommandListData (text :String, icon :Class, iconObject :IFlexDisplayObject,
labelField :String, uid :String, owner :IUIComponent,
rowIndex :int = 0, columnIndex :int = 0)
{
super(text, icon, labelField, uid, owner, rowIndex, columnIndex);
this.iconObject = iconObject;
}
}
}
@@ -0,0 +1,61 @@
//
// $Id$
//
// Nenya library - tools for developing networked games
// Copyright (C) 2002-2012 Three Rings Design, Inc., All Rights Reserved
// http://code.google.com/p/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.flex.menuClasses {
import flash.display.DisplayObject;
import mx.controls.Menu;
import mx.controls.menuClasses.IMenuDataDescriptor;
import mx.controls.menuClasses.MenuItemRenderer;
public class CommandMenuItemRenderer extends MenuItemRenderer
{
public function CommandMenuItemRenderer ()
{
super();
}
override protected function commitProperties () :void
{
super.commitProperties();
// Note: a menu with no items will have a null listData
if ((icon == null) && (listData is CommandListData)) {
icon = CommandListData(listData).iconObject;
if (icon != null) {
addChild(DisplayObject(icon));
}
}
if (label.visible && (listData != null)) {
var dataDescriptor :IMenuDataDescriptor = Menu(listData.owner).dataDescriptor;
var typeVal :String = dataDescriptor.getType(data);
if (typeVal == "title") {
dataDescriptor.setEnabled(data, false);
label.enabled = false;
label.styleName = "menuTitle";
invalidateDisplayList();
}
}
}
}
}
@@ -0,0 +1,28 @@
//
// $Id$
//
// Nenya library - tools for developing networked games
// Copyright (C) 2002-2012 Three Rings Design, Inc., All Rights Reserved
// http://code.google.com/p/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.media {
public interface Tickable
{
function tick (tickStamp :int) :void;
}
}
@@ -0,0 +1,79 @@
//
// $Id$
//
// Nenya library - tools for developing networked games
// Copyright (C) 2002-2012 Three Rings Design, Inc., All Rights Reserved
// http://code.google.com/p/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.media {
import flash.events.TimerEvent;
import flash.utils.Timer;
import com.threerings.util.Arrays;
/**
* Registers objects that wish to be sent a tick() call once per frame.
*/
public class Ticker
{
public function start () :void
{
_t.addEventListener(TimerEvent.TIMER, handleTimer);
_start = new Date().time;
_t.start();
}
public function stop () :void
{
_t.removeEventListener(TimerEvent.TIMER, handleTimer);
_t.stop();
}
public function handleTimer (event :TimerEvent) :void
{
tick(int(new Date().time - _start));
}
protected function tick (tickStamp :int) :void
{
for each (var tickable :Tickable in _tickables) {
tickable.tick(tickStamp);
}
}
public function registerTickable (tickable :Tickable) :void
{
_tickables.push(tickable);
}
public function removeTickable (tickable :Tickable) :void
{
Arrays.removeFirst(_tickables, tickable);
}
/** Everyone who wants to hear about our ticks. */
protected var _tickables :Array = [];
/** A timer that will fire every "frame". */
protected var _t :Timer = new Timer(1);
/** What time our ticker started running - tickStamps will be relative to this time. */
protected var _start :Number;
}
}
@@ -0,0 +1,170 @@
//
// $Id$
//
// Nenya library - tools for developing networked games
// Copyright (C) 2002-2012 Three Rings Design, Inc., All Rights Reserved
// http://code.google.com/p/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.media.image {
import com.threerings.util.Log;
import com.threerings.util.Map;
import com.threerings.util.Maps;
import com.threerings.util.RandomUtil;
import com.threerings.util.StringUtil;
public class ClassRecord
{
private const log :Log = Log.getLog(ClassRecord);
/** An integer identifier for this class. */
public var classId :int;
/** The name of the color class. */
public var name :String;
/** The source color to use when recoloring colors in this class. */
public var source :uint;
/** Data identifying the range of colors around the source color
* that will be recolored when recoloring using this class. */
public var range :Array;
/** The default starting legality value for this color class. See
* {@link ColorRecord#starter}. */
public var starter :Boolean;
/** The default colorId to use for recoloration in this class, or
* 0 if there is no default defined. */
public var defaultId :int;
/** A table of target colors included in this class. */
public var colors :Map = Maps.newMapOf(int);
/** Used when parsing the color definitions. */
public function addColor (record :ColorRecord) :void
{
// validate the color id
if (record.colorId > 127) {
log.warning("Refusing to add color record; colorId > 127",
"class", this, "record", record);
} else if (colors.containsKey(record.colorId)) {
log.warning("Refusing to add duplicate colorId",
"class", this, "record", record, "existing", colors.get(record.colorId));
} else {
record.cclass = this;
colors.put(record.colorId, record);
}
}
/**
* Translates a color identified in string form into the id that should be used to look up
* its information. Throws an exception if no color could be found that associates with
* that name.
*/
public function getColorId (name :String) :int
{
// Check if the string is itself a number
var id :int = name as int;
if (colors.containsKey(id)) {
return id;
}
// Look for name matches among all colors
for each (var color :ColorRecord in colors.values()) {
if (StringUtil.compareIgnoreCase(color.name, name) == 0) {
return color.colorId;
}
}
// That input wasn't a color
throw new Error("No color named '" + name + "'", 0);
}
/** Returns a random starting id from the entries in this class. */
public function randomStartingColor () :ColorRecord
{
// figure out our starter ids if we haven't already
if (_starters == null) {
_starters = [];
for each (var color :ColorRecord in colors) {
if (color.starter) {
_starters.push(color);
}
}
}
// sanity check
if (_starters.length < 1) {
log.warning("Requested random starting color from colorless component class",
"class", this);
return null;
}
// return a random entry from the array
return RandomUtil.pickRandom(_starters);
}
/**
* Get the default ColorRecord defined for this color class, or
* null if none.
*/
public function getDefault () :ColorRecord
{
return colors.get(defaultId);
}
public function toString () :String
{
return "[id=" + classId + ", name=" + name + ", source=#" +
StringUtil.toHex(source & 0xFFFFFF, 6) +
", range=" + StringUtil.toString(range) +
", starter=" + starter + ", colors=" +
StringUtil.toString(colors.values()) + "]";
}
public static function fromXml (xml :XML) :ClassRecord
{
var rec :ClassRecord = new ClassRecord();
rec.classId = xml.@classId;
for each (var colorXml :XML in xml.color) {
rec.colors.put(int(colorXml.@colorId), ColorRecord.fromXml(colorXml, rec));
}
rec.name = xml.@name;
var srcStr :String = xml.@source;
rec.source = parseInt(srcStr.substr(1, srcStr.length - 1), 16);
rec.range = toNumArray(xml.@range);
rec.defaultId = xml.@defaultId;
return rec;
}
protected static function toNumArray (str :String) :Array
{
if (str == null) {
return null;
}
return str.split(",").map(function(element :*, index :int, arr :Array) :Number {
return Number(element);
});
}
protected var _starters :Array;
}
}
@@ -0,0 +1,215 @@
//
// $Id$
//
// Nenya library - tools for developing networked games
// Copyright (C) 2002-2012 Three Rings Design, Inc., All Rights Reserved
// http://code.google.com/p/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.media.image {
import com.threerings.util.Log;
import com.threerings.util.Map;
import com.threerings.util.Maps;
import com.threerings.util.StringUtil;
public class ColorPository
{
private const log :Log = Log.getLog(ColorPository);
public function getClasses () :Array
{
return _classes.values();
}
/**
* Returns an array containing the records for the colors in the
* specified class.
*/
public function getColors (className :String) :Array
{
// make sure the class exists
var record :ClassRecord = getClassRecordByName(className);
if (record == null) {
return null;
}
// create the array
return record.colors.values();
}
/**
* Returns an array containing the ids of the colors in the specified
* class.
*/
public function getColorIds (className :String) :Array
{
// make sure the class exists
var record :ClassRecord = getClassRecordByName(className);
if (record == null) {
return null;
}
return record.colors.values().map(
function(element :ColorRecord, index :int, arr :Array) :int {
return element.colorId;
});
}
/**
* Returns true if the specified color is legal for use at character creation time. false is
* always returned for non-existent colors or classes.
*/
public function isLegalStartColor (classId :int, colorId :int) :Boolean
{
var color :ColorRecord = getColorRecord(classId, colorId);
return (color == null) ? false : color.starter;
}
/**
* Returns a random starting color from the specified color class.
*/
public function getRandomStartingColor (className :String) :ColorRecord
{
// make sure the class exists
var record :ClassRecord = getClassRecordByName(className);
return (record == null) ? null : record.randomStartingColor();
}
/**
* Looks up a colorization by id.
*/
public function getColorization (classId :int, colorId :int) :Colorization
{
var color :ColorRecord = getColorRecord(classId, colorId);
return (color == null) ? null : color.getColorization();
}
/**
* Looks up a colorization by color print.
*/
public function getColorizationByPrint (colorPrint :int) :Colorization
{
return getColorization(colorPrint >> 8, colorPrint & 0xFF);
}
/**
* Looks up a colorization by class and color names.
*/
public function getColorizationByName (className :String, colorName :String) :Colorization
{
var crec :ClassRecord = getClassRecordByName(className);
if (crec != null) {
var colorId :int = crec.getColorId(colorName);
var color :ColorRecord = crec.colors.get(colorId);
if (color != null) {
return color.getColorization();
}
}
return null;
}
/**
* Looks up a colorization by class and color Id.
*/
public function getColorizationByNameAndId (className :String, colorId :int) :Colorization
{
var crec :ClassRecord = getClassRecordByName(className);
if (crec != null) {
var color :ColorRecord = crec.colors.get(colorId);
if (color != null) {
return color.getColorization();
}
}
return null;
}
/**
* Loads up a colorization class by name and logs a warning if it doesn't exist.
*/
public function getClassRecordByName (className :String) :ClassRecord
{
for each (var crec :ClassRecord in _classes.values()) {
if (crec.name == className) {
return crec;
}
}
log.warning("No such color class", "class", className, new Error());
return null;
}
/**
* Looks up the requested color class record.
*/
public function getClassRecord (classId :int) :ClassRecord
{
return _classes.get(classId);
}
/**
* Looks up the requested color record.
*/
public function getColorRecord (classId :int, colorId :int) :ColorRecord
{
var record :ClassRecord = getClassRecord(classId);
if (record == null) {
// if they request color class zero, we assume they're just
// decoding a blank colorprint, otherwise we complain
if (classId != 0) {
log.warning("Requested unknown color class",
"classId", classId, "colorId", colorId, new Error());
}
return null;
}
return record.colors.get(colorId);
}
/**
* Looks up the requested color record by class & color names.
*/
public function getColorRecordByName (className :String, colorName :String) :ColorRecord
{
var record :ClassRecord = getClassRecordByName(className);
if (record == null) {
log.warning("Requested unknown color class",
"className", className, "colorName", colorName, new Error());
return null;
}
var colorId :int = record.getColorId(colorName);
return record.colors.get(colorId);
}
/**
* Loads up a serialized color pository from the supplied resource
* manager.
*/
public static function fromXml (xml :XML) :ColorPository
{
var pos :ColorPository = new ColorPository();
for each (var classXml :XML in xml.elements("class")) {
pos._classes.put(int(classXml.@classId), ClassRecord.fromXml(classXml));
}
return pos;
}
/** Our mapping from class names to class records. */
protected var _classes :Map = Maps.newMapOf(int);
}
}
@@ -0,0 +1,103 @@
//
// $Id$
//
// Nenya library - tools for developing networked games
// Copyright (C) 2002-2012 Three Rings Design, Inc., All Rights Reserved
// http://code.google.com/p/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.media.image {
import com.threerings.util.Log;
import com.threerings.util.StringUtil;
public class ColorRecord
{
private const log :Log = Log.getLog(ColorRecord);
/** The colorization class to which we belong. */
public var cclass :ClassRecord;
/** A unique colorization identifier (used in fingerprints). */
public var colorId :int;
/** The name of the target color. */
public var name :String;
/** Data indicating the offset (in HSV color space) from the
* source color to recolor to this color. */
public var offsets :Array;
/** Tags this color as a legal starting color or not. This is a
* shameful copout, placing application-specific functionality
* into a general purpose library class. */
public var starter :Boolean;
/**
* Returns a value that is the composite of our class id and color
* id which can be used to identify a colorization record. This
* value will always be a positive integer that fits into 16 bits.
*/
public function getColorPrint () :int
{
return ((cclass.classId << 8) | colorId);
}
/**
* Returns the data in this record configured as a colorization
* instance.
*/
public function getColorization () :Colorization
{
return new Colorization(getColorPrint(), cclass.source,
cclass.range, offsets);
}
public function toString () :String
{
return "[id=" + colorId + ", name=" + name +
", offsets=" + StringUtil.toString(offsets) +
", starter=" + starter + "]";
}
public static function fromXml (xml :XML, cclass :ClassRecord) :ColorRecord
{
var rec :ColorRecord = new ColorRecord();
rec.colorId = xml.@colorId;
rec.name = xml.@name;
rec.offsets = toNumArray(xml.@offsets);
rec.starter = xml.@starter;
rec.cclass = cclass;
return rec;
}
protected static function toNumArray (str :String) :Array
{
if (str == null) {
return null;
}
return str.split(",").map(function(element :String, index :int, arr :Array) :Number {
return Number(StringUtil.trim(element));
});
}
/** Our data represented as a colorization. */
protected var _zation :Colorization;
}
}
@@ -0,0 +1,185 @@
//
// $Id$
//
// Nenya library - tools for developing networked games
// Copyright (C) 2002-2012 Three Rings Design, Inc., All Rights Reserved
// http://code.google.com/p/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.media.image {
import com.threerings.display.ColorUtil;
import com.threerings.util.Hashable;
import com.threerings.util.Short;
import com.threerings.util.StringUtil;
public class Colorization
implements Hashable
{
/** Every colorization must have a unique id that can be used to
* compare a particular colorization record with another. */
public var colorizationId :int;
/** The root color for the colorization. */
public var rootColor :uint;
/** The range around the root color that will be colorized (in
* delta-hue, delta-saturation, delta-value. Note that this range is inclusive. */
public var range :Array;
/** The adjustments to make to hue, saturation and value. */
public var offsets :Array;
/**
* Constructs a colorization record with the specified identifier.
*/
public function Colorization (colorizationId :int, rootColor :uint,
range :Array, offsets :Array)
{
this.colorizationId = colorizationId;
this.rootColor = rootColor;
this.range = range;
this.offsets = offsets;
// compute our HSV and fixed HSV
_hsv = ColorUtil.RGBtoHSB(ColorUtil.getRed(rootColor), ColorUtil.getGreen(rootColor),
ColorUtil.getBlue(rootColor));
_fhsv = toFixedHSV(_hsv);
}
/**
* Returns the root color adjusted by the colorization.
*/
public function getColorizedRoot () :uint
{
return recolorColor(_hsv);
}
/**
* Adjusts the supplied color by the offests in this colorization,
* taking the appropriate measures for hue (wrapping it around) and
* saturation and value (clipping).
*
* @return the RGB value of the recolored color.
*/
public function recolorColor (hsv :Array) :uint
{
// for hue, we wrap around
var hue :Number = hsv[0] + offsets[0];
if (hue > 1.0) {
hue -= 1.0;
}
// otherwise we clip
var sat :Number = Math.min(Math.max(hsv[1] + offsets[1], 0), 1);
var val :Number = Math.min(Math.max(hsv[2] + offsets[2], 0), 1);
// convert back to RGB space
return ColorUtil.HSBtoRGB(hue, sat, val);
}
/**
* Returns true if this colorization matches the supplied color, false
* otherwise.
*
* @param hsv the HSV values for the color in question.
* @param fhsv the HSV values converted to fixed point via {@link
* #toFixedHSV} for the color in question.
*/
public function matches (hsv :Array, fhsv :Array) :Boolean
{
// check to see that this color is sufficiently "close" to the
// root color based on the supplied distance parameters
if (distance(fhsv[0], _fhsv[0], Short.MAX_VALUE) >
range[0] * Short.MAX_VALUE) {
return false;
}
// saturation and value don't wrap around like hue
if (Math.abs(_hsv[1] - hsv[1]) > range[1] ||
Math.abs(_hsv[2] - hsv[2]) > range[2]) {
return false;
}
return true;
}
public function hashCode () :int
{
return colorizationId ^ rootColor;
}
public function equals (other :Object) :Boolean
{
if (other is Colorization) {
return (Colorization(other)).colorizationId == colorizationId;
} else {
return false;
}
}
public function toString () :String
{
return String(colorizationId);
}
/**
* Returns a long string representation of this colorization.
*/
public function toVerboseString () :String
{
return StringUtil.toString(this);
}
/**
* Converts floating point HSV values to a fixed point integer
* representation.
*
* @param hsv the HSV values to be converted.
* @param fhsv the destination array into which the fixed values will
* be stored. If this is null, a new array will be created of the
* appropriate length.
*
* @return the <code>fhsv</code> parameter if it was non-null or the
* newly created target array.
*/
public static function toFixedHSV (hsv :Array) :Array
{
var fhsv :Array = new Array(hsv.length);
for (var ii :int = 0; ii < hsv.length; ii++) {
fhsv[ii] = int(hsv[ii] * Short.MAX_VALUE);
}
return fhsv;
}
/**
* Returns the distance between the supplied to numbers modulo N.
*/
public static function distance (a :int, b :int, N :int) :int
{
return (a > b) ? Math.min(a - b, b + N - a) : Math.min(b - a, a + N - b);
}
/** Fixed HSV values for our root color; used when calculating
* recolorizations using this colorization. */
protected var _fhsv :Array;
/** HSV values for our root color; used when calculating
* recolorizations using this colorization. */
protected var _hsv :Array;
}
}
@@ -0,0 +1,212 @@
//
// $Id$
//
// Nenya library - tools for developing networked games
// Copyright (C) 2002-2012 Three Rings Design, Inc., All Rights Reserved
// http://code.google.com/p/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.media.image {
import flash.utils.ByteArray;
import com.threerings.display.ColorUtil;
import com.threerings.util.Log;
import com.threerings.util.StringUtil;
import com.threerings.util.F;
public class PngRecolorUtil
{
private static const log :Log = Log.getLog(PngRecolorUtil);
protected static function isPng (bytes :ByteArray) :Boolean
{
if (bytes.length < PNG_HEADER.length) {
return false;
}
for (var ii :int = 0; ii < PNG_HEADER.length; ii++) {
if (PNG_HEADER[ii] != bytes[ii]) {
return false;
}
}
return true;
}
/**
* Find the required chunks in the bytes that represent a PNG image. All of the chunks that
* are succesfully found are pushed into the returned array in the order in which they were
* found in the image.
*/
protected static function findChunks (pngBytes :ByteArray, chunkTypes :Array) :Array
{
var chunks :Array = [];
if (chunkTypes.length < 1) {
return chunks;
}
for (var ii :int = PNG_HEADER.length; ii < pngBytes.length;) {
pngBytes.position = ii;
var chunk :PngChunk =
new PngChunk(ii + 8, pngBytes.readInt(), pngBytes.readUTFBytes(4));
var idx :int = chunkTypes.indexOf(chunk.type);
if (idx >= 0) {
chunks.push(chunk);
if (chunkTypes.length == 1) {
return chunks;
} else {
chunkTypes.splice(idx, 1);
}
}
// 4 bytes each for length, chunk type and CRC
ii += 12 + chunk.length;
}
return chunks;
}
public static function recolorPNG (pngBytes :ByteArray,
colors :Array/* of Colorization */) :ByteArray
{
if (colors == null || colors.length == 0) {
return pngBytes;
}
// first, lets make sure we really have a PNG
if (!isPng(pngBytes)) {
log.warning("recolorPNG received invalid pngBytes", new Error());
return pngBytes;
}
var chunks :Array = findChunks(pngBytes, [HEADER_CHUNK, PALETTE_CHUNK]);
if (chunks.length != 2 || (chunks[0] as PngChunk).type != HEADER_CHUNK) {
log.warning("recolorPNG received an unexected PNG format", "requiredChunksFound",
chunks.length, new Error());
return pngBytes;
}
var header :PngChunk = chunks[0] as PngChunk;
var palette :PngChunk = chunks[1] as PngChunk;
// Check to make sure the header declares this to be an indexed-color PNG
var colorType :int = pngBytes[header.idx + COLOR_TYPE_IDX];
if (colorType != INDEXED_COLOR_TYPE) {
log.warning(
"Color Type in PNG header is not indexed-color", "type", colorType, new Error());
return pngBytes;
}
initializeCRCTable();
// CRC starts with chunk type
var crc :uint = 0xFFFFFFFF;
for (var ii :int = 0; ii < 4; ii++) {
crc = crcCalc(crc, PALETTE_CHUNK.charCodeAt(ii));
}
pngBytes.position = palette.idx;
for (ii = palette.idx; ii < palette.idx + palette.length; ii += 3) {
var red :uint = pngBytes.readUnsignedByte();
var green :uint = pngBytes.readUnsignedByte();
var blue :uint = pngBytes.readUnsignedByte();
var hsv :Array = ColorUtil.RGBtoHSB(red, green, blue);
var fhsv :Array = Colorization.toFixedHSV(hsv);
for each (var color :Colorization in colors) {
if (color != null && color.matches(hsv, fhsv)) {
var newRgb :uint = color.recolorColor(hsv);
pngBytes[ii] = red = ColorUtil.getRed(newRgb);
pngBytes[ii + 1] = green = ColorUtil.getGreen(newRgb);
pngBytes[ii + 2] = blue = ColorUtil.getBlue(newRgb);
break;
}
}
crc = F.foldl([red, green, blue], crc, crcCalc);
}
crc = uint(crc ^ uint(0xFFFFFFFF));
// write out the CRC for the modified color table
pngBytes.position = palette.idx + palette.length;
pngBytes.writeUnsignedInt(crc);
return pngBytes;
}
protected static function initializeCRCTable () :void
{
if (_crcTable != null) {
return;
}
_crcTable = [];
for (var n :uint = 0; n < 256; n++)
{
var c :uint = n;
for (var k :uint = 0; k < 8; k++)
{
if (c & 1) {
c = uint(uint(0xedb88320) ^ uint(c >>> 1));
} else {
c = uint(c >>> 1);
}
}
_crcTable[n] = c;
}
}
protected static function crcCalc (crc :uint, byte :uint) :uint
{
return uint(_crcTable[(crc ^ byte) & uint(0xFF)] ^ uint(crc >>> 8));
}
/** The amount to pad error messages by. */
private static const ERROR_PADDING :int = 5;
/** Various Png-recolor related constants. */
protected static const PNG_HEADER :Array =
[ 0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A ];
protected static const PALETTE_CHUNK :String = "PLTE";
protected static const HEADER_CHUNK :String = "IHDR";
protected static const COLOR_TYPE_IDX :int = 9;
protected static const INDEXED_COLOR_TYPE :int = 3;
protected static var _crcTable :Array;
}
}
import com.threerings.util.StringUtil;
/** Used for recoloring pngs. */
class PngChunk
{
/** The index of the beginning of the data section of the chunk. */
public var idx :int;
/** The length of the data section of the chunk. */
public var length :int;
/** The type of this chunk. */
public var type :String;
public function PngChunk (idx :int, length :int, type :String)
{
this.idx = idx;
this.length = length;
this.type = type;
}
public function toString () :String
{
return StringUtil.simpleToString(this);
}
}
@@ -0,0 +1,83 @@
//
// $Id$
//
// Nenya library - tools for developing networked games
// Copyright (C) 2002-2012 Three Rings Design, Inc., All Rights Reserved
// http://code.google.com/p/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.media.tile {
public class BaseTile extends Tile
{
/**
* Returns whether or not this tile can be walked upon by character
* sprites.
*/
public function isPassable () :Boolean
{
return _passable;
}
/**
* Configures this base tile as passable or impassable.
*/
public function setPassable (passable :Boolean) :void
{
_passable = passable;
}
/**
* Returns the x offset into the tile image of the origin (which will be aligned with the
* bottom center of the origin tile) or <code>Integer.MIN_VALUE</code> if the origin is not
* explicitly specified and should be computed from the image size and tile footprint.
*/
public override function getOriginX () :int
{
return getWidth()/2;
}
/**
* Returns the y offset into the tile image of the origin (which will be aligned with the
* bottom center of the origin tile) or <code>Integer.MIN_VALUE</code> if the origin is not
* explicitly specified and should be computed from the image size and tile footprint.
*/
public override function getOriginY () :int
{
return getHeight();
}
/**
* Returns the object footprint width in tile units.
*/
public override function getBaseWidth () :int
{
return 1;
}
/**
* Returns the object footprint height in tile units.
*/
public override function getBaseHeight () :int
{
return 1;
}
/** Whether the tile is passable. */
protected var _passable :Boolean = true;
}
}
@@ -0,0 +1,92 @@
//
// $Id$
//
// Nenya library - tools for developing networked games
// Copyright (C) 2002-2012 Three Rings Design, Inc., All Rights Reserved
// http://code.google.com/p/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.media.tile {
import com.threerings.util.Arrays;
import com.threerings.util.StringUtil;
public class BaseTileSet extends SwissArmyTileSet
{
/**
* Sets the passability information for the tiles in this tileset.
* Each entry in the array corresponds to the tile at that tile index.
*/
public function setPassability (passable :Array) :void
{
_passable = passable;
}
/**
* Returns the passability information for the tiles in this tileset.
*/
public function getPassability () :Array
{
return _passable;
}
override protected function createTile () :Tile
{
return new BaseTile();
}
override protected function initTile (tile :Tile, tileIndex :int, zations :Array) :void
{
super.initTile(tile, tileIndex, zations);
(BaseTile(tile)).setPassable(_passable[tileIndex]);
}
override protected function toStringBuf (buf :String) :String
{
buf = super.toStringBuf(buf);
buf = buf.concat(", passable=", StringUtil.toString(_passable));
return buf;
}
public static function fromXml (xml :XML) :SwissArmyTileSet
{
var set :BaseTileSet = new BaseTileSet();
set.populateFromXml(xml);
return set;
}
override protected function populateFromXml (xml :XML) :void
{
super.populateFromXml(xml);
_passable = toBoolArray(xml.passable);
}
override public function populateClone (clone :TileSet) :void
{
super.populateClone(clone);
var bClone :BaseTileSet = BaseTileSet(clone);
bClone._passable = _passable == null ? null : Arrays.copyOf(_passable);
}
override public function createClone () :TileSet
{
return new BaseTileSet();
}
/** Whether each tile is passable. */
protected var _passable :Array;
}
}
@@ -0,0 +1,36 @@
//
// $Id$
//
// Nenya library - tools for developing networked games
// Copyright (C) 2002-2012 Three Rings Design, Inc., All Rights Reserved
// http://code.google.com/p/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.media.tile {
import com.threerings.media.image.Colorization;
public interface Colorizer
{
/**
* Returns the colorization to be used for the specified named colorization class.
*
* @param index the index of the colorization being requested in the tileset's colorization
* list.
*/
function getColorization (index :int, zation :String) :Colorization;
}
}
@@ -0,0 +1,261 @@
//
// $Id$
//
// Nenya library - tools for developing networked games
// Copyright (C) 2002-2012 Three Rings Design, Inc., All Rights Reserved
// http://code.google.com/p/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.media.tile {
import nochump.util.zip.ZipEntry;
import flash.net.URLLoader;
import flash.net.URLLoaderDataFormat;
import flash.net.URLRequest;
import flash.events.Event;
import flash.utils.ByteArray;
import com.threerings.io.ObjectInputStream;
import com.threerings.media.tile.TileDataPack;
import com.threerings.media.tile.bundle.TileSetBundle;
import com.threerings.util.DataPack;
import com.threerings.util.Integer;
import com.threerings.util.Log;
import com.threerings.util.Maps;
import com.threerings.util.Map;
import com.threerings.util.Set;
import com.threerings.util.StringUtil;
public class DataPackTileSetRepository
implements TileSetRepository, TileSetIdMap
{
private static var log :Log = Log.getLog(DataPackTileSetRepository);
public function DataPackTileSetRepository (tileSetUrl :String)
{
_rootUrl = tileSetUrl;
loadTileSetMeta(tileSetUrl + "metadata.jar");
}
protected function loadTileSetMeta (tileSetMetaUrl :String) :void
{
_metaPack = new TileDataPack(tileSetMetaUrl, metaLoadingComplete);
}
protected function parseTileSetMap (map :String) :void
{
var lines :Array = map.split("\n");
var tileCount :int = int(lines[0]);
for (var ii :int = 1; ii < lines.length; ii++) {
var line :String = lines[ii];
var toks :Array = line.split(" := ");
if (toks.length >= 2) {
var id :int = int(toks[1]);
var name :String = String(toks[0]);
_nameMap.put(name, id);
}
}
}
/**
* Handle the successful completion of datapack loading.
*
* @private
*/
protected function metaLoadingComplete (event :Event) :void
{
parseTileSetMap(_metaPack.getFileAsString("tilesetmap.txt"));
for each (var entry :ZipEntry in _metaPack.getFiles()) {
var bundleName :String = entry.name;
if (StringUtil.endsWith(bundleName, "/bundle.xml")) {
loadTileSetsFromPack(bundleName.substr(0, bundleName.length - 11));
}
}
// Notify them all and clear our list.
for each (var func :Function in _notifyOnLoad) {
func();
}
_notifyOnLoad = [];
}
public function notifyOnLoad (func :Function) :void
{
_notifyOnLoad.push(func);
}
public function enumerateTileSetIds () :Array
{
return _idMap.keys();
}
public function enumerateTileSets () :Array
{
return _idMap.values();
}
public function getTileSet (tileSetId :int) :TileSet
{
var tileSet :TileSet = _idMap.get(tileSetId);
// We don't have this tileset loaded just yet.... start grabbing the data pack.
var packName :String = _packMap.get(tileSetId);
if (packName == null) {
log.warning("Attempted to load unknown tile set", "tileSetId", tileSetId);
throw new NoSuchTileSetError(tileSetId);
}
var pack :TileDataPack = _packs.get(packName);
if (pack == null || !pack.isComplete()) {
loadPack(packName, tileSetId, tileSet);
// Loading the pack should've put us in the map, so let's go retrieve it...
tileSet = _idMap.get(tileSetId);
} else {
tileSet.setImageProvider(_packs.get(packName));
}
return tileSet;
}
protected function loadPack (packName :String, tileSetId :int, tileSet :TileSet) :void
{
var pack :TileDataPack = _packs.get(packName);
if (pack != null && pack.isComplete()) {
// Already loaded
return;
}
var completeListener :Function = function () :void {
tileSet.setImageProvider(_packs.get(packName));
};
var listeners :Array = _packListeners.get(packName);
if (listeners == null) {
listeners = [];
_packListeners.put(packName, listeners);
var packCompleteListener :Function = function () :void {
for each (var listener :Function in listeners) {
listener();
}
_packListeners.remove(packName);
}
pack = new TileDataPack(getPackUrl(packName), packCompleteListener);
_packs.put(packName, pack);
}
listeners.push(completeListener);
}
protected function loadTileSetsFromPack (packName :String) :void
{
var xml :XML = _metaPack.getFileAsXML(packName + "/bundle.xml");
var bundle :TileSetBundle = TileSetBundle.fromXml(xml, this);
bundle.forEach(function(key :*, val :*) :void {
_idMap.put(key, val);
_packMap.put(key, packName);
});
}
protected function getPackUrl (packName :String) :String
{
return _rootUrl + packName + "/bundle.jar";
}
public function getTileSetId (setName :String) :int
{
return _nameMap.get(setName);
}
public function getTileSetByName (setName :String) :TileSet
{
return getTileSet(getTileSetId(setName));
}
protected function getPackName (name :String) :String
{
var lastSlashIdx :int = name.lastIndexOf("/");
if (lastSlashIdx == -1) {
return name;
} else {
return name.substring(0, lastSlashIdx).toLowerCase();
}
}
public function ensureLoaded (tileSets :Set, completeCallback :Function,
progressCallback :Function) :void
{
var completeCt :int = 0;
var size :int = tileSets.size();
// Handles the completion of a single tileset, which if it's our last may call the callback.
function completeOneSet () :void {
completeCt++;
progressCallback(completeCt / Number(size));
if (completeCt >= tileSets.size()) {
completeCallback();
// Ensure we stop making callbacks.
completeCallback = null;
progressCallback = null;
}
};
tileSets.forEach(function (setId :int) :void {
var thisSet :TileSet = getTileSet(setId);
if (thisSet.isLoaded()) {
completeOneSet();
} else {
thisSet.notifyOnLoad(completeOneSet);
}
});
}
/** Contains the tileset meta-data for all tilesets. */
protected var _metaPack : DataPack;
/** Maps name to tileSetId. */
protected var _nameMap :Map = Maps.newMapOf(String);
/** Maps tileSetId to actual TileSet. */
protected var _idMap : Map = Maps.newMapOf(int);
/** Maps tileSetId to DataPack name. */
protected var _packMap :Map = Maps.newMapOf(int);
/** The URL for our tilesets. */
protected var _rootUrl :String;
/** Map of pack name to listeners waiting for that pack to resolve. */
protected var _packListeners :Map = Maps.newMapOf(String);
/** The map of packs we've already loaded up and resolved. */
protected var _packs :Map = Maps.newMapOf(String);
/** Everyone who cares when we're loaded. */
protected var _notifyOnLoad :Array = [];
}
}
@@ -0,0 +1,52 @@
//
// $Id$
//
// Nenya library - tools for developing networked games
// Copyright (C) 2002-2012 Three Rings Design, Inc., All Rights Reserved
// http://code.google.com/p/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.media.tile {
import flash.display.DisplayObject;
import flash.geom.Rectangle;
public interface ImageProvider
{
/**
* Returns the raw tileset image with the specified path.
*
* @param path the path that identifies the desired image (corresponds to the image path from
* the tileset).
* @param zations if non-null, colorizations to apply to the source image before returning it.
*/
function getTileSetImage (path :String, zations :Array, callback :Function) :void;
/**
* Obtains the tile image with the specified path in the form of a {@link Mirage}. It should be
* cropped from the tileset image identified by the supplied path.
*
* @param path the path that identifies the desired image (corresponds to the image path from
* the tileset).
* @param bounds if non-null, the region of the image to be returned as a mirage. If null, the
* entire image should be returned.
* @param zations if non-null, colorizations to apply to the image before converting it into a
* mirage.
*/
function getTileImage (path :String, bounds :Rectangle, zations :Array, callback :Function)
:void;
}
}
@@ -0,0 +1,32 @@
//
// $Id$
//
// Nenya library - tools for developing networked games
// Copyright (C) 2002-2012 Three Rings Design, Inc., All Rights Reserved
// http://code.google.com/p/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.media.tile {
public class NoSuchTileSetError extends Error
{
public function NoSuchTileSetError (tileSet :*)
{
super((tileSet is int) ? ("No tile set with id '" + int(tileSet) + "'") :
("No tile set named'" + String(tileSet) + "'"));
}
}
}
@@ -0,0 +1,212 @@
//
// $Id$
//
// Nenya library - tools for developing networked games
// Copyright (C) 2002-2012 Three Rings Design, Inc., All Rights Reserved
// http://code.google.com/p/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.media.tile {
import flash.geom.Point;
import com.threerings.media.tile.Tile;
import com.threerings.util.Arrays;
import com.threerings.util.DirectionUtil;
import com.threerings.util.Integer;
import com.threerings.util.StringUtil;
public class ObjectTile extends Tile
{
/**
* Returns the object footprint width in tile units.
*/
public override function getBaseWidth () :int
{
return _base.x;
}
/**
* Returns the object footprint height in tile units.
*/
public override function getBaseHeight () :int
{
return _base.y;
}
/**
* Sets the object footprint in tile units.
*/
public function setBase (width :int, height :int) :void
{
_base.x = width;
_base.y = height;
}
/**
* Returns the x offset into the tile image of the origin (which will be aligned with the
* bottom center of the origin tile) or <code>Integer.MIN_VALUE</code> if the origin is not
* explicitly specified and should be computed from the image size and tile footprint.
*/
public override function getOriginX () :int
{
return _origin.x;
}
/**
* Returns the y offset into the tile image of the origin (which will be aligned with the
* bottom center of the origin tile) or <code>Integer.MIN_VALUE</code> if the origin is not
* explicitly specified and should be computed from the image size and tile footprint.
*/
public override function getOriginY () :int
{
return _origin.y;
}
/**
* Sets the offset in pixels from the origin of the tile image to the origin of the object.
* The object will be rendered such that its origin is at the bottom center of its origin
* tile. If no origin is specified, the bottom of the image is aligned with the bottom of the
* origin tile and the left side of the image is aligned with the left edge of the left-most
* base tile.
*/
public function setOrigin (x :int, y :int) :void
{
_origin.x = x;
_origin.y = y;
}
/**
* Returns this object tile's default render priority.
*/
public function getPriority () :int
{
return _priority;
}
/**
* Sets this object tile's default render priority.
*/
public function setPriority (priority :int) :void
{
_priority = priority;
}
/**
* Configures the "spot" associated with this object.
*/
public function setSpot (x :int, y :int, orient :int) :void
{
_spot = new Point(x, y);
_sorient = orient;
}
/**
* Returns true if this object has a spot.
*/
public function hasSpot () :Boolean
{
return (_spot != null);
}
/**
* Returns the x-coordinate of the "spot" associated with this object.
*/
public function getSpotX () :int
{
return (_spot == null) ? 0 : _spot.x;
}
/**
* Returns the x-coordinate of the "spot" associated with this object.
*/
public function getSpotY () :int
{
return (_spot == null) ? 0 : _spot.y;
}
/**
* Returns the orientation of the "spot" associated with this object.
*/
public function getSpotOrient () :int
{
return _sorient;
}
/**
* Returns the list of constraints associated with this object, or <code>null</code> if the
* object has no constraints.
*/
public function getConstraints () :Array
{
return _constraints;
}
/**
* Checks whether this object has the given constraint.
*/
public function hasConstraint (constraint :String) :Boolean
{
return (_constraints == null) ? false :
Arrays.contains(_constraints, constraint);
}
/**
* Configures this object's constraints.
*/
public function setConstraints (constraints :Array) :void
{
_constraints = constraints;
}
protected override function toStringBuf (buf :String) :String
{
buf = super.toStringBuf(buf);
buf.concat(", base=", StringUtil.toString(_base));
buf.concat(", origin=", StringUtil.toString(_origin));
buf.concat(", priority=", _priority);
if (_spot != null) {
buf.concat(", spot=", StringUtil.toString(_spot));
buf.concat(", sorient=");
buf.concat(DirectionUtil.toShortString(_sorient));
}
if (_constraints != null) {
buf.concat(", constraints=", StringUtil.toString(_constraints));
}
return buf;
}
/** The object footprint width in unit tile units. */
protected var _base :Point = new Point(1, 1);
/** The offset from the origin of the tile image to the object's origin or MIN_VALUE if the
* origin should be calculated based on the footprint. */
protected var _origin :Point = new Point(Integer.MIN_VALUE, Integer.MIN_VALUE);
/** This object tile's default render priority. */
protected var _priority :int;
/** The coordinates of the "spot" associated with this object. */
protected var _spot :Point;
/** The orientation of the "spot" associated with this object. */
protected var _sorient :int;
/** The list of constraints associated with this object. */
protected var _constraints :Array;
}
}
@@ -0,0 +1,328 @@
//
// $Id$
//
// Nenya library - tools for developing networked games
// Copyright (C) 2002-2012 Three Rings Design, Inc., All Rights Reserved
// http://code.google.com/p/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.media.tile {
import com.threerings.util.Arrays;
import com.threerings.util.StringUtil;
/**
* The object tileset supports the specification of object information for object tiles in
* addition to all of the features of the swiss army tileset.
*
* @see ObjectTile
*/
public class ObjectTileSet extends SwissArmyTileSet
implements RecolorableTileSet
{
/** A constraint prefix indicating that the object must have empty space in
* the suffixed direction (N, E, S, or W). */
public static const SPACE :String = "SPACE_";
/** A constraint indicating that the object is a surface (e.g., table). */
public static const SURFACE :String = "SURFACE";
/** A constraint indicating that the object must be placed on a surface. */
public static const ON_SURFACE :String = "ON_SURFACE";
/** A constraint prefix indicating that the object is a wall facing the
* suffixed direction (N, E, S, or W). */
public static const WALL :String = "WALL_";
/** A constraint prefix indicating that the object must be placed on a
* wall facing the suffixed direction (N, E, S, or W). */
public static const ON_WALL :String = "ON_WALL_";
/** A constraint prefix indicating that the object must be attached to a
* wall facing the suffixed direction (N, E, S, or W). */
public static const ATTACH :String = "ATTACH_";
/** The low suffix for walls and attachments. Low attachments can be placed
* on low or normal walls; normal attachments can only be placed on normal
* walls. */
public static const LOW :String = "_LOW";
/**
* Sets the widths (in unit tile count) of the objects in this tileset. This must be
* accompanied by a call to {@link #setObjectHeights}.
*/
public function setObjectWidths (objectWidths :Array) :void
{
_owidths = objectWidths;
}
/**
* Sets the heights (in unit tile count) of the objects in this tileset. This must be
* accompanied by a call to {@link #setObjectWidths}.
*/
public function setObjectHeights (objectHeights :Array) :void
{
_oheights = objectHeights;
}
/**
* Sets the x offset in pixels to the image origin.
*/
public function setXOrigins (xorigins :Array) :void
{
_xorigins = xorigins;
}
/**
* Sets the y offset in pixels to the image origin.
*/
public function setYOrigins (yorigins :Array) :void
{
_yorigins = yorigins;
}
/**
* Sets the default render priorities for our object tiles.
*/
public function setPriorities (priorities :Array) :void
{
_priorities = priorities;
}
/**
* Provides a set of colorization classes that apply to objects in this tileset.
*/
public function setColorizations (zations :Array) :void
{
_ozations = zations;
}
/**
* Sets the x offset to the "spots" associated with our object tiles.
*/
public function setXSpots (xspots :Array) :void
{
_xspots = xspots;
}
/**
* Sets the y offset to the "spots" associated with our object tiles.
*/
public function setYSpots (yspots :Array) :void
{
_yspots = yspots;
}
/**
* Sets the orientation of the "spots" associated with our object
* tiles.
*/
public function setSpotOrients (sorients :Array) :void
{
_sorients = sorients;
}
/**
* Sets the lists of constraints associated with our object tiles.
*/
public function setConstraints (constraints :Array) :void
{
_constraints = constraints;
}
/**
* Returns the x coordinate of the spot associated with the specified tile index.
*/
public function getXSpot (tileIdx :int) :int
{
return (_xspots == null) ? 0 : _xspots[tileIdx];
}
/**
* Returns the y coordinate of the spot associated with the specified tile index.
*/
public function getYSpot (tileIdx :int) :int
{
return (_yspots == null) ? 0 : _yspots[tileIdx];
}
/**
* Returns the orientation of the spot associated with the specified tile index.
*/
public function getSpotOrient (tileIdx :int) :int
{
return (_sorients == null) ? 0 : _sorients[tileIdx];
}
/**
* Returns the list of constraints associated with the specified tile index, or
* <code>null</code> if the index has no constraints.
*/
public function getConstraints (tileIdx :int) :Array
{
return (_constraints == null) ? null : _constraints[tileIdx];
}
/**
* Checks whether the tile at the specified index has the given constraint.
*/
public function hasConstraint (tileIdx :int, constraint :String) :Boolean
{
return (_constraints == null) ? false :
Arrays.contains(_constraints[tileIdx], constraint);
}
// documentation inherited from interface RecolorableTileSet
public function getColorizations () :Array
{
return _ozations;
}
override protected function toStringBuf (buf :String) :String
{
buf = super.toStringBuf(buf);
buf = buf.concat(", owidths=", StringUtil.toString(_owidths));
buf = buf.concat(", oheights=", StringUtil.toString(_oheights));
buf = buf.concat(", xorigins=", StringUtil.toString(_xorigins));
buf = buf.concat(", yorigins=", StringUtil.toString(_yorigins));
buf = buf.concat(", prios=", StringUtil.toString(_priorities));
buf = buf.concat(", zations=", StringUtil.toString(_ozations));
buf = buf.concat(", xspots=", StringUtil.toString(_xspots));
buf = buf.concat(", yspots=", StringUtil.toString(_yspots));
buf = buf.concat(", sorients=", StringUtil.toString(_sorients));
buf = buf.concat(", constraints=", StringUtil.toString(_constraints));
return buf;
}
override protected function getTileColorizations (tileIndex :int, rizer :Colorizer) :Array
{
var zations :Array = null;
if (rizer != null && _ozations != null) {
zations = new Array(_ozations.length);
for (var ii :int = 0; ii < _ozations.length; ii++) {
zations[ii] = rizer.getColorization(ii, _ozations[ii]);
}
}
return zations;
}
override protected function createTile () :Tile
{
return new ObjectTile();
}
override protected function initTile (tile :Tile, tileIndex :int, zations :Array) :void
{
super.initTile(tile, tileIndex, zations);
var otile :ObjectTile = ObjectTile(tile);
if (_owidths != null) {
otile.setBase(_owidths[tileIndex], _oheights[tileIndex]);
}
if (_xorigins != null) {
otile.setOrigin(_xorigins[tileIndex], _yorigins[tileIndex]);
}
if (_priorities != null) {
otile.setPriority(_priorities[tileIndex]);
}
if (_xspots != null) {
otile.setSpot(_xspots[tileIndex], _yspots[tileIndex], _sorients[tileIndex]);
}
if (_constraints != null) {
otile.setConstraints(_constraints[tileIndex]);
}
}
public static function fromXml (xml :XML) :SwissArmyTileSet
{
var set :ObjectTileSet = new ObjectTileSet();
set.populateFromXml(xml);
return set;
}
override protected function populateFromXml (xml :XML) :void
{
super.populateFromXml(xml);
_owidths = toIntArray(xml.objectWidths);
_oheights = toIntArray(xml.objectHeights);
_xorigins = toIntArray(xml.xOrigins);
_yorigins = toIntArray(xml.yOrigins);
_priorities = toIntArray(xml.priorities);
_ozations = toStrArray(xml.zations);
_xspots = toIntArray(xml.xSpots);
_yspots = toIntArray(xml.ySpots);
_sorients = toIntArray(xml.sorients);
var constraintStrArr :Array = toStrArray(xml.constraints);
if (constraintStrArr != null) {
_constraints =
constraintStrArr.map(function(element :*, index :int, arr :Array) :Array {
return element.split("|");
});
}
}
override public function populateClone (clone :TileSet) :void
{
super.populateClone(clone);
var oClone :ObjectTileSet = ObjectTileSet(clone);
oClone._owidths = _owidths == null ? null : Arrays.copyOf(_owidths);
oClone._oheights = _oheights == null ? null : Arrays.copyOf(_oheights);
oClone._xorigins = _xorigins == null ? null : Arrays.copyOf(_xorigins);
oClone._yorigins = _yorigins == null ? null : Arrays.copyOf(_yorigins);
oClone._priorities = _priorities == null ? null : Arrays.copyOf(_priorities);
oClone._ozations = _ozations == null ? null : Arrays.copyOf(_ozations);
oClone._xspots = _xspots == null ? null : Arrays.copyOf(_xspots);
oClone._yspots = _yspots == null ? null : Arrays.copyOf(_yspots);
oClone._sorients = _sorients == null ? null : Arrays.copyOf(_sorients);
oClone._constraints = _constraints == null ? null : Arrays.copyOf(_constraints);
}
override public function createClone () :TileSet
{
return new ObjectTileSet();
}
/** The width (in tile units) of our object tiles. */
protected var _owidths :Array;
/** The height (in tile units) of our object tiles. */
protected var _oheights :Array;
/** The x offset in pixels to the origin of the tile images. */
protected var _xorigins :Array;
/** The y offset in pixels to the origin of the tile images. */
protected var _yorigins :Array;
/** The default render priorities of our objects. */
protected var _priorities :Array;
/** Colorization classes that apply to our objects. */
protected var _ozations :Array;
/** The x offset to the "spots" associated with our tiles. */
protected var _xspots :Array;
/** The y offset to the "spots" associated with our tiles. */
protected var _yspots :Array;
/** The orientation of the "spots" associated with our tiles. */
protected var _sorients :Array;
/** Lists of constraints associated with our tiles. */
protected var _constraints :Array;
}
}
@@ -0,0 +1,32 @@
//
// $Id$
//
// Nenya library - tools for developing networked games
// Copyright (C) 2002-2012 Three Rings Design, Inc., All Rights Reserved
// http://code.google.com/p/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.media.tile {
public interface RecolorableTileSet
{
/**
* Returns the colorization classes that should be used to recolor
* objects in this tileset.
*/
function getColorizations () :Array
}
}
@@ -0,0 +1,265 @@
//
// $Id$
//
// Nenya library - tools for developing networked games
// Copyright (C) 2002-2012 Three Rings Design, Inc., All Rights Reserved
// http://code.google.com/p/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.media.tile {
import flash.geom.Point;
import flash.geom.Rectangle;
import com.threerings.util.Arrays;
import com.threerings.util.StringUtil;
/**
* The swiss army tileset supports a diverse variety of tiles in the tileset image. Each row can
* contain varying numbers of tiles and each row can have its own width and height. Tiles can be
* separated from the edge of the tileset image by some border offset and can be separated from one
* another by a gap distance.
*/
public class SwissArmyTileSet extends TileSet
{
public override function getTileCount () :int
{
return _numTiles;
}
public override function computeTileBounds (tileIndex :int) :Rectangle
{
// find the row number containing the sought-after tile
var ridx :int;
var tcount :int;
var ty :int;
var tx :int;
ridx = tcount = 0;
// start tile image position at image start offset
tx = _offsetPos.x;
ty = _offsetPos.y;
while ((tcount += _tileCounts[ridx]) < tileIndex + 1) {
// increment tile image position by row height and gap distance
ty += (_heights[ridx++] + _gapSize.y);
}
// determine the horizontal index of this tile in the row
var xidx :int = tileIndex - (tcount - _tileCounts[ridx]);
// final image x-position is based on tile width and gap distance
tx += (xidx * (_widths[ridx] + _gapSize.x));
// Log.info("Computed tile bounds [tileIndex=" + tileIndex +
// ", ridx=" + ridx + ", xidx=" + xidx + ", tx=" + tx + ", ty=" + ty + "].");
// crop the tile-sized image chunk from the full image
return new Rectangle(tx, ty, _widths[ridx], _heights[ridx]);
}
/**
* Sets the tile counts which are the number of tiles in each row of the tileset image. Each
* row can have an arbitrary number of tiles.
*/
public function setTileCounts (tileCounts :Array) :void
{
_tileCounts = tileCounts;
// compute our total tile count
computeTileCount();
}
/**
* Returns the tile count settings.
*/
public function getTileCounts () :Array
{
return _tileCounts;
}
/**
* Computes our total tile count from the individual counts for each row.
*/
protected function computeTileCount () :void
{
// compute our number of tiles
_numTiles = 0;
for each (var count :int in _tileCounts) {
_numTiles += count;
}
}
/**
* Sets the tile widths for each row. Each row can have tiles of a different width.
*/
public function setWidths (widths :Array) :void
{
_widths = widths;
}
/**
* Returns the width settings.
*/
public function getWidths () :Array
{
return _widths;
}
/**
* Sets the tile heights for each row. Each row can have tiles of a different height.
*/
public function setHeights (heights :Array) :void
{
_heights = heights;
}
/**
* Returns the height settings.
*/
public function getHeights () :Array
{
return _heights;
}
/**
* Sets the offset in pixels of the upper left corner of the first tile in the first row. If
* the tileset image has a border, this can be set to account for it.
*/
public function setOffsetPos (offsetPos :Point) :void
{
_offsetPos = offsetPos;
}
/**
* Sets the size of the gap between tiles (in pixels). If the tiles have space between them,
* this can be set to account for it.
*/
public function setGapSize (gapSize :Point) :void
{
_gapSize = gapSize;
}
protected override function toStringBuf (buf :String) :String
{
buf = super.toStringBuf(buf);
buf = buf.concat(", widths=", StringUtil.toString(_widths));
buf = buf.concat(", heights=", StringUtil.toString(_heights));
buf = buf.concat(", tileCounts=", StringUtil.toString(_tileCounts));
buf = buf.concat(", offsetPos=", StringUtil.toString(_offsetPos));
buf = buf.concat(", gapSize=", StringUtil.toString(_gapSize));
return buf;
}
public static function fromXml (xml :XML) :SwissArmyTileSet
{
var set :SwissArmyTileSet = new SwissArmyTileSet();
set.populateFromXml(xml);
return set;
}
protected override function populateFromXml (xml :XML) :void
{
super.populateFromXml(xml);
_tileCounts = toIntArray(xml.tileCounts);
_widths = toIntArray(xml.widths);
_heights = toIntArray(xml.heights);
var offsetPosArr :Array = toIntArray(xml.offsetPos);
if (offsetPosArr != null) {
_offsetPos = new Point(offsetPosArr[0], offsetPosArr[1]);
} else {
_offsetPos = new Point(0, 0);
}
var gapSizeArr :Array = toIntArray(xml.gapSize);
if (gapSizeArr != null) {
_gapSize = new Point(gapSizeArr[0], gapSizeArr[1]);
} else {
_gapSize = new Point(0, 0);
}
computeTileCount();
}
protected static function toStrArray (str :String) :Array
{
if (str == null || str.length == 0) {
return null;
}
return str.split(",").map(function(element :String, index :int, arr :Array) :String {
return StringUtil.trim(element);
});
}
protected static function toBoolArray (str :String) :Array
{
if (str == null || str.length == 0) {
return null;
}
return str.split(",").map(function(element :*, index :int, arr :Array) :Boolean {
return (StringUtil.trim(element) == "1");
});
}
protected static function toIntArray (str :String) :Array
{
if (str == null || str.length == 0) {
return null;
}
return str.split(",").map(function(element :*, index :int, arr :Array) :int {
return int(StringUtil.trim(element));
});
}
override public function populateClone (clone :TileSet) :void
{
super.populateClone(clone);
var saClone :SwissArmyTileSet = SwissArmyTileSet(clone);
saClone._tileCounts = _tileCounts == null ? null : Arrays.copyOf(_tileCounts);
saClone._numTiles = _numTiles;
saClone._widths = _widths == null ? null : Arrays.copyOf(_widths);
saClone._heights = _heights == null ? null : Arrays.copyOf(_heights);
saClone._offsetPos = _offsetPos;
saClone._gapSize = _gapSize;
}
override public function createClone () :TileSet
{
return new SwissArmyTileSet();
}
/** The number of tiles in each row. */
protected var _tileCounts :Array;
/** The number of tiles in the tileset. */
protected var _numTiles :int;
/** The width of the tiles in each row in pixels. */
protected var _widths :Array;
/** The height of the tiles in each row in pixels. */
protected var _heights :Array;
/** The offset distance (x, y) in pixels from the top-left of the image to the start of the
* first tile image. */
protected var _offsetPos :Point = new Point();
/** The distance (x, y) in pixels between each tile in each row horizontally, and between each
* row of tiles vertically. */
protected var _gapSize :Point = new Point();
}
}
@@ -0,0 +1,151 @@
//
// $Id$
//
// Nenya library - tools for developing networked games
// Copyright (C) 2002-2012 Three Rings Design, Inc., All Rights Reserved
// http://code.google.com/p/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.media.tile {
import flash.display.Bitmap;
import flash.display.BitmapData;
import flash.display.DisplayObject;
public class Tile
{
/** The key associated with this tile. */
public var key :Tile_Key;
/**
* Configures this tile with its tile image.
*/
public function setImage (image :DisplayObject) :void
{
_image = image;
// Notify them all and clear our list.
for each (var func :Function in _notifyOnLoad) {
func(this);
}
}
public function getImage () :DisplayObject
{
if (_image is Bitmap) {
// TODO - handle this more consistently...
return new Bitmap(Bitmap(_image).bitmapData);
} else {
return _image;
}
}
/**
* Returns the width of this tile.
*/
public function getWidth () :int
{
return _image.width;
}
/**
* Returns the height of this tile.
*/
public function getHeight () :int
{
return _image.height;
}
/**
* Returns true if the specified coordinates within this tile contains
* a non-transparent pixel.
*/
public function hitTest (x :int, y :int) :Boolean
{
return _image.hitTestPoint(x, y, true);
}
public function toString () :String
{
return "[" + toStringBuf(new String()) + "]";
}
/**
* This should be overridden by derived classes (which should be sure
* to call <code>super.toString()</code>) to append the derived class
* specific tile information to the string buffer.
*/
protected function toStringBuf (buf :String) :String
{
if (_image == null) {
return buf.concat("null-image");
} else {
return buf.concat(_image.width, "x", _image.height);
}
}
public function notifyOnLoad (func :Function) :void
{
if (_image != null) {
func(this);
} else {
_notifyOnLoad.push(func);
}
}
/**
* Returns the x offset into the tile image of the origin (which will be aligned with the
* bottom center of the origin tile) or <code>Integer.MIN_VALUE</code> if the origin is not
* explicitly specified and should be computed from the image size and tile footprint.
*/
public function getOriginX () :int
{
throw new Error("abstract");
}
/**
* Returns the y offset into the tile image of the origin (which will be aligned with the
* bottom center of the origin tile) or <code>Integer.MIN_VALUE</code> if the origin is not
* explicitly specified and should be computed from the image size and tile footprint.
*/
public function getOriginY () :int
{
throw new Error("abstract")
}
/**
* Returns the object footprint width in tile units.
*/
public function getBaseWidth () :int
{
throw new Error("abstract");
}
/**
* Returns the object footprint height in tile units.
*/
public function getBaseHeight () :int
{
throw new Error("abstract");
}
/** Our tileset image. */
protected var _image :DisplayObject;
/** Everyone who cares when we're loaded. */
protected var _notifyOnLoad :Array = [];
}
}
@@ -0,0 +1,129 @@
//
// $Id$
//
// Nenya library - tools for developing networked games
// Copyright (C) 2002-2012 Three Rings Design, Inc., All Rights Reserved
// http://code.google.com/p/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.media.tile {
import flash.display.Bitmap;
import flash.display.BitmapData;
import flash.display.DisplayObject;
import flash.geom.Point;
import flash.geom.Rectangle;
import flash.events.Event;
import flash.utils.ByteArray;
import nochump.util.zip.ZipEntry;
import nochump.util.zip.ZipError;
import nochump.util.zip.ZipFile;
import com.threerings.media.image.PngRecolorUtil;
import com.threerings.util.DataPack;
import com.threerings.util.Map;
import com.threerings.util.Maps;
import com.threerings.util.MultiLoader;
import com.threerings.util.StringUtil;
/**
* Like a normal data pack, but we don't deal with any data, and all our filenames are a direct
* translation, so we need no metadata file. We also serve as a tile ImageProvider.
*/
public class TileDataPack extends DataPack
implements ImageProvider
{
public function TileDataPack (
source :Object, completeListener :Function = null, errorListener :Function = null)
{
super(source, completeListener, errorListener);
}
protected override function bytesAvailable (bytes :ByteArray) :void
{
bytes.position = 0;
try {
_zip = new ZipFile(bytes);
} catch (zipError :ZipError) {
dispatchError("Unable to read datapack: " + zipError.message);
return;
}
// Put something there so we know we're loaded.
_metadata = <xml></xml>;
// yay, we're completely loaded!
dispatchEvent(new Event(Event.COMPLETE));
}
/**
* We have no metadata, so everything is undefined.
*/
public override function getData (name :String, formatType :String = null) :*
{
return undefined;
}
/**
* We have no XML metadata, so our filename is exactly the original name.
*/
protected override function getFileName (name :String) :String
{
return name;
}
public function getTileSetImage (path :String, zations :Array, callback :Function) :void
{
var key :String = zations == null ? path : path + ":" + StringUtil.toString(zations);
if (_cache.containsKey(key)) {
callback(_cache.get(key));
} else if (_pending.containsKey(key)) {
_pending.get(key).push(callback);
} else {
_pending.put(key, [callback]);
MultiLoader.getContents(PngRecolorUtil.recolorPNG(getFile(path), zations),
function(result :Bitmap) :void {
_cache.put(key, result);
for each (var func :Function in _pending.remove(key)) {
func(result);
}
});
}
}
public function getTileImage (path :String, bounds :Rectangle, zations :Array,
callback :Function) :void
{
getTileSetImage(path, zations, function(result :Bitmap) :void {
var data :BitmapData =
new BitmapData(bounds.width, bounds.height, true, 0x00000000);
data.copyPixels(Bitmap(result).bitmapData, bounds, new Point(0, 0));
callback(new Bitmap(data));
});
}
/** Cache of images loaded from this data pack. */
protected var _cache :Map = Maps.newMapOf(String);
/** Any images we're in the process of resolving, map their key to a list of listeners*/
protected var _pending :Map = Maps.newMapOf(String);
}
}
@@ -0,0 +1,147 @@
//
// $Id$
//
// Nenya library - tools for developing networked games
// Copyright (C) 2002-2012 Three Rings Design, Inc., All Rights Reserved
// http://code.google.com/p/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.media.tile {
import com.threerings.util.Log;
import com.threerings.util.Set;
/**
* The tile manager provides a simplified interface for retrieving and caching tiles. Tiles can be
* loaded in two different ways. An application can load a tileset by hand, specifying the path to
* the tileset image and all of the tileset metadata necessary for extracting the image tiles, or
* it can provide a tileset repository which loads up tilesets using whatever repository mechanism
* is implemented by the supplied repository. In the latter case, tilesets are loaded by a unique
* identifier.
*
* <p> Loading tilesets by hand is intended for things like toolbar icons or games with a single
* set of tiles (think Stratego, for example). Loading tilesets from a repository supports games
* with vast numbers of tiles to which more tiles may be added on the fly (think the tiles for an
* isometric-display graphical MUD).
*/
public class TileManager
{
private static var log :Log = Log.getLog(TileManager);
/**
* Sets the tileset repository that will be used by the tile manager when tiles are requested
* by tileset id.
*/
public function setTileSetRepository (setrep :TileSetRepository) :void
{
_setrep = setrep;
}
/**
* Returns the tileset repository currently in use.
*/
public function getTileSetRepository () :TileSetRepository
{
return _setrep;
}
/**
* Returns the tileset with the specified id. Tilesets are fetched from the tileset repository
* supplied via {@link #setTileSetRepository}, and are subsequently cached.
*
* @param tileSetId the unique identifier for the desired tileset.
*
* @exception NoSuchTileSetError thrown if no tileset exists with the specified id or if
* an underlying error occurs with the tileset repository's persistence mechanism.
*/
public function getTileSet (tileSetId :int) :TileSet
{
// make sure we have a repository configured
if (_setrep == null) {
throw new NoSuchTileSetError(tileSetId);
}
try {
return _setrep.getTileSet(tileSetId);
} catch (pe :Error) {
log.warning("Failure loading tileset", "id", tileSetId, pe);
throw new NoSuchTileSetError(tileSetId);
}
// Unreachable.
return null;
}
/**
* Returns the tileset with the specified name.
*
* @throws NoSuchTileSetError if no tileset with the specified name is available via our
* configured tile set repository.
*/
public function getTileSetByName (name :String) :TileSet
{
// make sure we have a repository configured
if (_setrep == null) {
throw new NoSuchTileSetError(name);
}
try {
return _setrep.getTileSetByName(name);
} catch (pe :Error) {
log.warning("Failure loading tileset", "name", name, "error", pe);
throw new NoSuchTileSetError(name);
}
// Unreachable.
return null;
}
/**
* Returns the {@link Tile} object with the specified fully qualified tile id. The supplied
* colorizer will be used to recolor the tile.
*
* @see TileUtil#getFQTileId
*/
public function getTile (fqTileId :int, rizer :Colorizer = null) :Tile
{
return getTileBySet(TileUtil.getTileSetId(fqTileId),
TileUtil.getTileIndex(fqTileId), rizer);
}
/**
* Returns the {@link Tile} object from the specified tileset at the specified index.
*
* @param tileSetId the tileset id.
* @param tileIndex the index of the tile to be retrieved.
*
* @return the tile object.
*/
public function getTileBySet (tileSetId :int, tileIndex :int, rizer :Colorizer) :Tile
{
var set :TileSet = getTileSet(tileSetId);
return set.getTile(tileIndex, rizer);
}
public function ensureLoaded (tileSets :Set, completeCallback :Function,
progressCallback :Function) :void
{
_setrep.ensureLoaded(tileSets, completeCallback, progressCallback);
}
/** The tile set repository. */
protected var _setrep :TileSetRepository;
}
}
@@ -0,0 +1,401 @@
//
// $Id$
//
// Nenya library - tools for developing networked games
// Copyright (C) 2002-2012 Three Rings Design, Inc., All Rights Reserved
// http://code.google.com/p/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.media.tile {
import flash.display.Bitmap;
import flash.display.DisplayObject;
import flash.geom.Rectangle;
import com.threerings.display.ImageUtil;
import com.threerings.io.ObjectInputStream;
import com.threerings.io.ObjectOutputStream;
import com.threerings.io.Streamable;
import com.threerings.media.image.Colorization;
import com.threerings.util.Cloneable;
import com.threerings.util.Hashable;
import com.threerings.util.Log;
import com.threerings.util.StringUtil;
import com.threerings.util.Throttle;
import com.threerings.util.maps.HashMap;
import com.threerings.util.maps.WeakValueMap;
public /* abstract */ class TileSet
implements Streamable, Hashable, Cloneable
{
private static var log :Log = Log.getLog(TileSet);
public function readObject (oin :ObjectInputStream) :void
{
_imagePath = oin.readUTF();
_name = oin.readUTF();
}
public function writeObject (oout :ObjectOutputStream) :void
{
oout.writeUTF(_imagePath);
oout.writeUTF(_name);
}
/**
* Configures this tileset with an image provider that it can use to load its tileset image.
* This will be called automatically when the tileset is fetched via the {@link TileManager}.
*/
public function setImageProvider (improv :ImageProvider) :void
{
_improv = improv;
if (_improv != null) {
for each (var arr :Array in _pending) {
initTile(arr[0], arr[1], arr[2]);
}
}
_pending = [];
loaded();
}
/**
* Returns the tileset name.
*/
public function getName () :String
{
return (_name == null) ? _imagePath : _name;
}
/**
* Specifies the tileset name.
*/
public function setName (name :String) :void
{
_name = name;
}
public function hashCode () :int
{
return StringUtil.hashCode(getName());
}
public function equals (other :Object) :Boolean
{
return other === this;
}
/**
* Sets the path to the image that will be used by this tileset. This must be called before the
* first call to {@link #getTile}.
*/
public function setImagePath (imagePath :String) :void
{
_imagePath = imagePath;
}
/**
* Returns the path to the composite image used by this tileset.
*/
public function getImagePath () :String
{
return _imagePath;
}
/**
* Returns the number of tiles in the tileset.
*/
public function getTileCount () :int
{
throw new Error("abstract");
}
/**
* Computes and fills in the bounds for the specified tile based on the mechanism used by the
* derived class to do such things. The width and height of the bounds should be the size of
* the tile image and the x and y offset should be the offset in the tileset image for the
* image data of the specified tile.
*
* @param tileIndex the index of the tile whose bounds are to be computed.
* @param bounds the rectangle object into which to fill the bounds.
*
* @return the rectangle passed into the bounds parameter.
*/
public function computeTileBounds (tileIndex :int) :Rectangle
{
throw new Error("abstract");
}
/**
* Creates a {@link Tile} object from this tileset corresponding to the specified tile id and
* returns that tile. A null tile will never be returned, but one with an error image may be
* returned if a problem occurs loading the underlying tileset image.
*
* @param tileIndex the index of the tile in the tileset. Tile indexes start with zero as the
* upper left tile and increase by one as the tiles move left to right and top to bottom over
* the source image.
* @param zations colorizations to be applied to the tile image prior to returning it. These
* may be null for uncolorized images.
*
* @return the tile object.
*/
public function getTile (tileIndex :int, zations :* = null) :Tile
{
// Default to using our default tileset colorizations.
if (zations == null) {
zations = _zations;
} else if (zations is Colorizer) {
zations = getTileColorizations(tileIndex, zations);
}
var tile :Tile = null;
// first look in the active set; if it's in use by anyone or in the cache, it will be in
// the active set
var key :Tile_Key = new Tile_Key(this, tileIndex, zations);
tile = _atiles.get(key);
// if it's not in the active set, it's not in memory; so load it
if (tile == null) {
tile = createTile();
tile.key = key;
initTile(tile, tileIndex, zations);
_atiles.put(tile.key, tile);
}
// periodically report our image cache performance
reportCachePerformance();
return tile;
}
/**
* Returns a prepared version of the image that would be used by the tile at the specified
* index. Because tilesets are often used simply to provide access to a collection of uniform
* images, this method is provided to bypass the creation of a {@link Tile} object when all
* that is desired is access to the underlying image.
*/
public function getTileImage (tileIndex :int, zations :Array, callback :Function)
:void
{
// If they weren't specified, get the default zations for this tile.
if (zations == null) {
zations = getTileColorizations(tileIndex, null);
}
var bounds :Rectangle = computeTileBounds(tileIndex);
var image :DisplayObject = null;
if (checkTileIndex(tileIndex)) {
if (_improv == null) {
log.warning("Aiya! Tile set missing image provider [path=" + _imagePath + "].");
callback(new Bitmap(ImageUtil.createErrorBitmap()));
} else {
_improv.getTileImage(_imagePath, bounds, zations,
function(result :DisplayObject) :void {
if (result == null) {
result = new Bitmap(ImageUtil.createErrorBitmap());
}
callback(result);
});
}
} else {
callback(ImageUtil.createErrorImage(bounds.width, bounds.height));
}
}
/**
* Returns colorizations for the specified tile image. The default is to return any
* colorizations associated with the tileset via a call to {@link #clone(Colorization[])},
* however derived classes may have dynamic colorization policies that look up colorization
* assignments from the supplied colorizer.
*/
protected function getTileColorizations (tileIndex :int, rizer :Colorizer) :Array
{
return _zations;
}
/**
* Used to ensure that the specified tile index is valid.
*/
protected function checkTileIndex (tileIndex :int) :Boolean
{
var tcount :int = getTileCount();
if (tileIndex >= 0 && tileIndex < tcount) {
return true;
} else {
log.warning("Requested invalid tile [tset=" + this + ", index=" + tileIndex + "].",
new Error());
return false;
}
}
/**
* Creates a blank tile of the appropriate type for this tileset.
*
* @return a blank tile ready to be populated with its image and metadata.
*/
protected function createTile () :Tile
{
return new Tile();
}
/**
* Initializes the supplied tile. Derived classes can override this method to add
* in their own tile information, but should be sure to call <code>super.initTile()</code>.
*
* @param tile the tile to initialize.
* @param tileIndex the index of the tile.
* @param zations the colorizations to be used when generating the tile image.
*/
protected function initTile (tile :Tile, tileIndex :int, zations :Array) :void
{
if (_improv != null) {
getTileImage(tileIndex, zations, function(result :DisplayObject) :void {
tile.setImage(result);
});
} else {
_pending.push([tile, tileIndex, zations]);
}
}
/**
* Reports statistics detailing the image manager cache performance and the current size of the
* cached images.
*/
protected function reportCachePerformance () :void
{
if (_improv == null ||
_cacheStatThrottle.throttleOp()) {
return;
}
// compute our estimated memory usage
var amem :int = 0;
var asize :int = 0;
// first total up the active tiles
for each (var tile :Tile in _atiles.values()) {
if (tile != null) {
asize++;
}
}
log.info("Tile caches [seen=" + _atiles.size() + ", asize=" + asize + "].");
}
public function toString () :String
{
return "[" + toStringBuf(new String()) + "]";
}
/**
* Derived classes can override this, calling <code>super.toString(buf)</code> and then
* appending additional information to the buffer.
*/
protected function toStringBuf (buf :String) :String
{
buf = buf.concat("name=", _name);
buf = buf.concat(", path=", _imagePath);
buf = buf.concat(", tileCount=", getTileCount());
return buf;
}
public function isLoaded () :Boolean
{
return _isLoaded;
}
public function loaded () :void
{
_isLoaded = true;
// Notify them all and clear our list.
for each (var func :Function in _notifyOnLoad) {
func(this);
}
_notifyOnLoad = [];
}
public function notifyOnLoad (func :Function) :void
{
if (isLoaded()) {
func(this);
} else {
_notifyOnLoad.push(func);
}
}
protected function populateFromXml (xml :XML) :void
{
_name = xml.@name;
_imagePath = xml.imagePath;
}
public function clone () :Object
{
var newSet :TileSet = createClone();
populateClone(newSet);
return newSet;
}
public function populateClone (clone :TileSet) :void
{
clone._isLoaded = _isLoaded;
clone._imagePath = _imagePath;
clone._name = _name;
clone._improv = _improv;
}
public function createClone () :TileSet
{
return new TileSet();
}
public function cloneWithZations (zations :Array) :TileSet
{
var newSet :TileSet = TileSet(clone());
newSet._zations = zations;
return newSet;
}
/** Whether all the media for this tileset is loaded and ready. */
protected var _isLoaded :Boolean;
/** The path to the file containing the tile images. */
protected var _imagePath :String;
/** The tileset name. */
protected var _name :String;
/** Colorizations to be applied to tiles created from this tileset. */
protected var _zations :Array; /* of */ Colorization;
/** The entity from which we obtain our tile image. */
protected var _improv :ImageProvider;
/** Everyone who cares when we're loaded. */
protected var _notifyOnLoad :Array = [];
/** Tiles awaiting an image provider. */
protected var _pending :Array = [];
/** A map containing weak references to all "active" tiles. */
protected static var _atiles :WeakValueMap = new WeakValueMap(new HashMap());
/** Throttle our cache status logging to once every 300 seconds. */
protected static var _cacheStatThrottle :Throttle = new Throttle(1, 300000);
}
}
@@ -0,0 +1,31 @@
//
// $Id$
//
// Nenya library - tools for developing networked games
// Copyright (C) 2002-2012 Three Rings Design, Inc., All Rights Reserved
// http://code.google.com/p/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.media.tile {
public interface TileSetIdMap
{
/**
* Returns the unique identifier for the named tileset.
*/
function getTileSetId (tileSetName :String) :int
}
}
@@ -0,0 +1,80 @@
//
// $Id$
//
// Nenya library - tools for developing networked games
// Copyright (C) 2002-2012 Three Rings Design, Inc., All Rights Reserved
// http://code.google.com/p/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.media.tile {
import com.threerings.util.Set;
public interface TileSetRepository {
/**
* Returns an iterator over the identifiers of all {@link TileSet}
* objects available.
*/
function enumerateTileSetIds () :Array;
/**
* Returns an iterator over all {@link TileSet} objects available.
*/
function enumerateTileSets () :Array;
/**
* Returns the {@link TileSet} with the specified tile set
* identifier. The repository is responsible for configuring the tile
* set with an image provider.
*
* @exception NoSuchTileSetException thrown if no tileset exists with
* the specified identifier.
* @exception PersistenceException thrown if an error occurs
* communicating with the underlying persistence mechanism.
*/
function getTileSet (tileSetId :int) :TileSet;
/**
* Returns the unique identifier of the {@link TileSet} with the
* specified tile set name.
*
* @exception NoSuchTileSetException thrown if no tileset exists with
* the specified name.
* @exception PersistenceException thrown if an error occurs
* communicating with the underlying persistence mechanism.
*/
function getTileSetId (setName :String) :int;
/**
* Returns the {@link TileSet} with the specified tile set name. The
* repository is responsible for configuring the tile set with an
* image provider.
*
* @exception NoSuchTileSetException thrown if no tileset exists with
* the specified name.
* @exception PersistenceException thrown if an error occurs
* communicating with the underlying persistence mechanism.
*/
function getTileSetByName (setName :String) :TileSet;
/**
* Ensures we are ready with quick-access to all the specified tilesets.
*/
function ensureLoaded (tileSets :Set, completeCallback :Function,
progressCallback :Function) :void;
}
}
@@ -0,0 +1,70 @@
//
// $Id$
//
// Nenya library - tools for developing networked games
// Copyright (C) 2002-2012 Three Rings Design, Inc., All Rights Reserved
// http://code.google.com/p/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.media.tile {
public class TileUtil
{
/**
* Generates a fully-qualified tile id given the supplied tileset id
* and tile index.
*/
public static function getFQTileId (tileSetId :int, tileIndex :int) :int
{
return (tileSetId << 16) | tileIndex;
}
/**
* Extracts the tile set id from the supplied fully qualified tile id.
*/
public static function getTileSetId (fqTileId :int) :int
{
return (fqTileId >> 16);
}
/**
* Extracts the tile index from the supplied fully qualified tile id.
*/
public static function getTileIndex (fqTileId :int) :int
{
return (fqTileId & 0xFFFF);
}
/**
* Compute some hash value for "randomizing" tileset picks
* based on x and y coordinates.
* NOTE: Because actionscript doesn't handle longs well, this does NOT match the implementation
* of the java version of getTileHash()
*
* @return a positive, seemingly random number based on x and y.
*/
public static function getTileHash (x :int, y :int) :int
{
var seed :int = (((x << 2) ^ y) ^ MULTIPLIER) & MASK;
var hash :int = (seed * MULTIPLIER + ADDEND) & MASK;
return hash >>> 10;
}
protected static const MULTIPLIER :int = 0x5E66D;
protected static const ADDEND :int = 0xB;
protected static const MASK :int = (1 << 16) - 1;
}
}
@@ -0,0 +1,67 @@
//
// $Id$
//
// Nenya library - tools for developing networked games
// Copyright (C) 2002-2012 Three Rings Design, Inc., All Rights Reserved
// http://code.google.com/p/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.media.tile {
import com.threerings.media.image.Colorization;
import com.threerings.util.Arrays;
import com.threerings.util.Hashable;
/** Used when caching tiles. */
public class Tile_Key
implements Hashable
{
public var tileSet :TileSet;
public var tileIndex :int;
public var zations :Array;
public function Tile_Key (tileSet :TileSet, tileIndex :int, zations :Array) {
this.tileSet = tileSet;
this.tileIndex = tileIndex;
this.zations = zations;
}
public function equals (other :Object) :Boolean
{
if (other is Tile_Key) {
var okey :Tile_Key = Tile_Key(other);
return (tileSet == okey.tileSet &&
tileIndex == okey.tileIndex &&
Arrays.equals(zations, okey.zations));
} else {
return false;
}
}
public function hashCode () :int
{
var code :int = (tileSet == null) ? tileIndex :
(tileSet.hashCode() ^ tileIndex);
var zcount :int = (zations == null) ? 0 : zations.length;
for each (var zation :Colorization in zations) {
if (zation != null) {
code ^= zation.hashCode();
}
}
return code;
}
}
}
@@ -0,0 +1,73 @@
//
// $Id$
//
// Nenya library - tools for developing networked games
// Copyright (C) 2002-2012 Three Rings Design, Inc., All Rights Reserved
// http://code.google.com/p/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.media.tile.bundle {
import flash.display.DisplayObject;
import com.threerings.io.ObjectOutputStream;
import com.threerings.io.ObjectInputStream;
import com.threerings.io.Streamable;
import com.threerings.util.DataPack;
import com.threerings.util.maps.DictionaryMap;
import com.threerings.media.tile.BaseTileSet;
import com.threerings.media.tile.ObjectTileSet;
import com.threerings.media.tile.SwissArmyTileSet;
import com.threerings.media.tile.TileSet;
import com.threerings.media.tile.TileSetIdMap;
public class TileSetBundle extends DictionaryMap
{
public function init (pack :DataPack) :void
{
_pack = pack;
}
public static function fromXml (xml :XML, idMap :TileSetIdMap) :TileSetBundle
{
var bundle :TileSetBundle = new TileSetBundle();
for each (var objXml :XML in xml.object.tileset) {
var objId :int = idMap.getTileSetId(objXml.@name);
bundle.put(objId, ObjectTileSet.fromXml(objXml));
}
for each (var baseXml :XML in xml.base.tileset) {
var baseId :int = idMap.getTileSetId(baseXml.@name);
bundle.put(baseId, BaseTileSet.fromXml(baseXml));
}
for each (var fringeXml :XML in xml.fringe.tileset) {
var fringeId :int = idMap.getTileSetId(fringeXml.@name);
bundle.put(fringeId, SwissArmyTileSet.fromXml(fringeXml));
}
return bundle;
}
/**
* Loads the image from our data pack.
*/
public function loadImage (path :String) :DisplayObject
{
return DisplayObject(_pack.getFile(path));
}
/** The data pack from which we can grab images. */
protected var _pack :DataPack;
}
}
@@ -0,0 +1,133 @@
//
// $Id$
//
// Nenya library - tools for developing networked games
// Copyright (C) 2002-2012 Three Rings Design, Inc., All Rights Reserved
// http://code.google.com/p/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.media.util {
import com.threerings.util.MathUtil;
/**
* Searches for the shortest traversable path between locations.
*/
public class AStarPathSearch
{
/**
* Creates a searching object using tpred to check for traversability and stepper to find
* all the valid adjacent spots and the stepping costs & estimates.
*/
public function AStarPathSearch (tpred :TraversalPred,
stepper :AStarPathSearch_Stepper = null)
{
_tpred = tpred;
if (stepper == null) {
_stepper = new AStarPathSearch_Stepper();
} else {
_stepper = stepper;
}
}
/**
* Return a list of <code>Point</code> objects representing a path from coordinates
* <code>(ax, by)</code> to <code>(bx, by)</code>, inclusive, determined by performing an
* A* search in the given scene's base tile layer. Assumes the starting and destination nodes
* are traversable by the specified traverser.
*
* @param trav the traverser to follow the path.
* @param longest the longest allowable path in tile traversals. This arg must be small enough
* that _stepper.getMaxCost(longest) < Integer.MAX_VALUE
* @param ax the starting x-position in tile coordinates.
* @param ay the starting y-position in tile coordinates.
* @param bx the ending x-position in tile coordinates.
* @param by the ending y-position in tile coordinates.
* @param partial if true, a partial path will be returned that gets us as close as we can to
* the goal in the event that a complete path cannot be located.
*
* @return the list of points in the path, or null if no path could be found.
*/
public function getPath (trav :Object, longest :int, ax :int, ay :int, bx :int, by :int,
partial :Boolean) :Array
{
var info :AStarPathSearch_Info = new AStarPathSearch_Info(_tpred, trav,
_stepper.getMaxCost(longest), bx, by);
// set up the starting node
var s :AStarPathSearch_Node = info.getNode(ax, ay);
s.g = 0;
s.h = _stepper.getDistanceEstimate(ax, ay, bx, by);
s.f = s.g + s.h;
// push starting node on the open list
info.open.add(s);
// track the best path
var bestdist :Number = Number.POSITIVE_INFINITY;
var bestpath :AStarPathSearch_Node = null;
// while there are more nodes on the open list
while (info.open.size() > 0) {
// pop the best node so far from open
var n :AStarPathSearch_Node = null;
info.open.forEach(function (val :AStarPathSearch_Node) :Boolean {
n = val;
return true;
});
info.open.remove(n);
// if node is a goal node
if (n.x == bx && n.y == by) {
// construct and return the acceptable path
return n.getNodePath();
} else if (partial) {
var pathdist :Number = MathUtil.distance(n.x, n.y, bx, by);
if (pathdist < bestdist) {
bestdist = pathdist;
bestpath = n;
}
}
// consider each successor of the node
_stepper.considerSteps(info, n, n.x, n.y);
// push the node on the closed list
info.closed.add(n);
}
// return the best path we could find if we were asked to do so
if (bestpath != null) {
return bestpath.getNodePath();
}
// no path found
return null;
}
/** In charge of determining if we can walk across various bits. */
protected var _tpred :TraversalPred;
/** In charge of finding all the steps from a given spot. */
protected var _stepper :AStarPathSearch_Stepper;
}
}
@@ -0,0 +1,180 @@
//
// $Id$
//
// Nenya library - tools for developing networked games
// Copyright (C) 2002-2012 Three Rings Design, Inc., All Rights Reserved
// http://code.google.com/p/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.media.util {
import com.threerings.util.Maps;
import com.threerings.util.Map;
import com.threerings.util.Set;
import com.threerings.util.Sets;
/**
* A holding class to contain the wealth of information referenced
* while performing an A* search for a path through a tile array.
*/
public class AStarPathSearch_Info
{
/** Knows whether or not tiles are traversable. */
public var tpred :TraversalPred;
/** The tile array dimensions. */
public var tilewid :int;
public var tilehei :int;
/** The traverser moving along the path. */
public var trav :Object;
/** The set of open nodes being searched. */
// TODO - This would benefit from a more efficient implementation of SortedSet, but for now
// it is good enough.
public var open :Set;
/** The set of closed nodes being searched. */
public var closed :Set;
/** The destination coordinates in the tile array. */
public var destx :int;
public var desty :int;
/** The maximum cost of any path that we'll consider. */
public var maxcost :int;
public function AStarPathSearch_Info (tpred :TraversalPred, trav :Object, maxcost :int,
destx :int, desty :int)
{
// save off references
this.tpred = tpred;
this.trav = trav;
this.destx = destx;
this.desty = desty;
// compute our maximum path cost
this.maxcost = maxcost;
// construct the open and closed lists
open = Sets.newSortedSetOf(AStarPathSearch_Node);
closed = Sets.newSetOf(AStarPathSearch_Node);
}
/**
* Returns whether moving from the given source to destination coordinates is a valid
* move.
*/
protected function isStepValid (sx :int, sy :int, dx :int, dy :int) :Boolean
{
// not traversable if the destination itself fails test
if (tpred is ExtendedTraversalPred) {
if (!ExtendedTraversalPred(tpred).canTraverseBetween(trav, sx, sy, dx, dy)) {
return false;
}
} else if (!isTraversable(dx, dy)) {
return false;
}
// if the step is diagonal, make sure the corners don't impede our progress
if ((Math.abs(dx - sx) == 1) && (Math.abs(dy - sy) == 1)) {
return isTraversable(dx, sy) && isTraversable(sx, dy);
}
// non-diagonals are always traversable
return true;
}
/**
* Returns whether the given coordinate is valid and traversable.
*/
protected function isTraversable (x :int, y :int) :Boolean
{
return tpred.canTraverse(trav, x, y);
}
/**
* Get or create the node for the specified point.
*/
public function getNode (x :int, y :int) :AStarPathSearch_Node
{
// note: this _could_ break for unusual values of x and y.
// perhaps use a IntTuple as a key? Bleah.
var key :int = (x << 16) | (y & 0xffff);
var node :AStarPathSearch_Node = _nodes.get(key);
if (node == null) {
node = new AStarPathSearch_Node(x, y);
_nodes.put(key, node);
}
return node;
}
/**
* Consider the step <code>(n.x, n.y)</code> to <code>(x, y)</code> for possible inclusion
* in the path.
*
* @param info the info object.
* @param node the originating node for the step.
* @param x the x-coordinate for the destination step.
* @param y the y-coordinate for the destination step.
*/
public function considerStep (node :AStarPathSearch_Node, x :int, y :int, cost :int,
stepper :AStarPathSearch_Stepper) :void
{
// skip node if it's outside the map bounds or otherwise impassable
if (!isStepValid(node.x, node.y, x, y)) {
return;
}
// calculate the new cost for this node
var newg :int = node.g + cost;
// make sure the cost is reasonable
if (newg > maxcost) {
return;
}
// retrieve the node corresponding to this location
var np :AStarPathSearch_Node = getNode(x, y);
// skip if it's already in the open or closed list or if its
// actual cost is less than the just-calculated cost
if ((open.contains(np) || closed.contains(np)) && np.g <= newg) {
return;
}
// remove the node from the open list since we're about to
// modify its score which determines its placement in the list
open.remove(np);
// update the node's information
np.parent = node;
np.g = newg;
np.h = stepper.getDistanceEstimate(np.x, np.y, destx, desty);
np.f = np.g + np.h;
// remove it from the closed list if it's present
closed.remove(np);
// add it to the open list for further consideration
open.add(np);
}
/** The nodes being considered in the path. */
protected var _nodes :Map = Maps.newMapOf(int);
}
}
@@ -0,0 +1,101 @@
//
// $Id$
//
// Nenya library - tools for developing networked games
// Copyright (C) 2002-2012 Three Rings Design, Inc., All Rights Reserved
// http://code.google.com/p/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.media.util {
import flash.geom.Point;
import com.threerings.util.Comparable;
public class AStarPathSearch_Node
implements Comparable
{
/** The node coordinates. */
public var x :int;
public var y :int;
/** The actual cheapest cost of arriving here from the start. */
public var g :int;
/** The heuristic estimate of the cost to the goal from here. */
public var h :int;
/** The score assigned to this node. */
public var f :int;
/** The node from which we reached this node. */
public var parent :AStarPathSearch_Node;
/** The node's monotonically-increasing unique identifier. */
public var id :int;
public function AStarPathSearch_Node (x :int, y :int)
{
this.x = x;
this.y = y;
id = _nextid++;
}
public function compareTo (o :Object) :int
{
var n :AStarPathSearch_Node = AStarPathSearch_Node(o);
var bf :int = n.f;
// since the set contract is fulfilled using the equality results returned here, and
// we'd like to allow multiple nodes with equivalent scores in our set, we explicitly
// define object equivalence as the result of object.equals(), else we use the unique
// node id since it will return a consistent ordering for the objects.
if (f == bf) {
return (this == n) ? 0 : (id - n.id);
}
return f - bf;
}
/**
* Return an array of <code>Point</code> objects detailing the path from the first node (the
* given node's ultimate parent) to the ending node (the given node itself.)
*
* @param node the ending node in the path.
*
* @return the list detailing the path.
*/
public function getNodePath () :Array
{
var cur :AStarPathSearch_Node = this;
var path :Array = [];
while (cur != null) {
// add to the head of the list since we're traversing from
// the end to the beginning
path.unshift(new Point(cur.x, cur.y));
// advance to the next node in the path
cur = cur.parent;
}
return path;
}
/** The next unique node id. */
protected static var _nextid :int = 0;
}
}
@@ -0,0 +1,79 @@
//
// $Id$
//
// Nenya library - tools for developing networked games
// Copyright (C) 2002-2012 Three Rings Design, Inc., All Rights Reserved
// http://code.google.com/p/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.media.util {
/**
* Considers all the possible steps the piece in question can take.
*/
public class AStarPathSearch_Stepper
{
public function AStarPathSearch_Stepper (considerDiagonals :Boolean = true)
{
_considerDiagonals = considerDiagonals;
}
public function getMaxCost (longest :int) :int
{
return longest * ADJACENT_COST;
}
/**
* Return a heuristic estimate of the cost to get from <code>(ax, ay)</code> to
* <code>(bx, by)</code>.
*/
public function getDistanceEstimate (ax :int, ay :int, bx :int, by :int) :int
{
// we're doing all of our cost calculations based on geometric distance times ten
var dx :int = bx - ax;
var dy :int = by - ay;
return int(Math.floor(ADJACENT_COST * Math.sqrt(dx * dx + dy * dy)));
}
/**
* Should call {@link #considerStep} in turn on all possible steps from the specified
* coordinates. No checking must be done as to whether the step is legal, that will be
* handled later. Just enumerate all possible steps.
*/
public function considerSteps (info :AStarPathSearch_Info, node :AStarPathSearch_Node,
x :int, y :int) :void
{
info.considerStep(node, x, y - 1, ADJACENT_COST, this);
info.considerStep(node, x, y + 1, ADJACENT_COST, this);
info.considerStep(node, x - 1, y, ADJACENT_COST, this);
info.considerStep(node, x + 1, y, ADJACENT_COST, this);
if (_considerDiagonals) {
info.considerStep(node, x - 1, y - 1, DIAGONAL_COST, this);
info.considerStep(node, x + 1, y - 1, DIAGONAL_COST, this);
info.considerStep(node, x - 1, y + 1, DIAGONAL_COST, this);
info.considerStep(node, x + 1, y + 1, DIAGONAL_COST, this);
}
}
protected var _considerDiagonals :Boolean;
/** The standard cost to move between nodes. */
protected static const ADJACENT_COST :int = 10;
/** The cost to move diagonally. */
protected static const DIAGONAL_COST :int = int(Math.sqrt((ADJACENT_COST * ADJACENT_COST) * 2));
}
}
@@ -0,0 +1,36 @@
//
// $Id$
//
// Nenya library - tools for developing networked games
// Copyright (C) 2002-2012 Three Rings Design, Inc., All Rights Reserved
// http://code.google.com/p/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.media.util {
/**
* Provides extended traversibility information when computing paths.
*/
public interface ExtendedTraversalPred extends TraversalPred
{
/**
* Requests to know if the specific traverser (which was provided in the call to
* {@link #getPath(TraversalPred,Object,int,int,int,int,int,boolean)}) can traverse from
* the specified source tile coordinate to the specified destination tile coordinate.
*/
function canTraverseBetween (traverser :Object, sx :int, sy :int, dx :int, dy :int) :Boolean;
}
}
@@ -0,0 +1,376 @@
//
// $Id$
//
// Nenya library - tools for developing networked games
// Copyright (C) 2002-2012 Three Rings Design, Inc., All Rights Reserved
// http://code.google.com/p/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.media.util {
import flash.geom.Point;
import flash.geom.Rectangle;
import com.threerings.util.ArrayIterator;
import com.threerings.util.DirectionCodes;
import com.threerings.util.DirectionUtil;
import com.threerings.util.Iterator;
import com.threerings.util.Log;
import com.threerings.util.MathUtil;
import com.threerings.util.StringUtil;
/**
* The line segment path is used to cause a pathable to follow a path that
* is made up of a sequence of line segments. There must be at least two
* nodes in any worthwhile path. The direction of the first node in the
* path is meaningless since the pathable begins at that node and will
* therefore never be heading towards it.
*/
public class LineSegmentPath
implements Path
{
private var log :Log = Log.getLog(LineSegmentPath);
/**
* Constructs an empty line segment path.
*/
public function LineSegmentPath ()
{
}
/**
* Constructs a line segment path that consists of a single segment
* connecting the point <code>(x1, y1)</code> with <code>(x2,
* y2)</code>. The orientation for the first node is set arbitrarily
* and the second node is oriented based on the vector between the two
* nodes (in top-down coordinates).
*/
public static function createWithInts (x1 :int, y1 :int, x2 :int, y2 :int) :LineSegmentPath
{
var path :LineSegmentPath = new LineSegmentPath();
path.addNode(x1, y1, DirectionCodes.NORTH);
var p1 :Point = new Point(x1, y1);
var p2 :Point = new Point(x2, y2);
var dir :int = DirectionUtil.getDirectionForPts(p1, p2);
path.addNode(x2, y2, dir);
return path;
}
/**
* Construct a line segment path between the two nodes with the
* specified direction.
*/
public static function createWithPoints (p1 :Point, p2 :Point, dir :int) :LineSegmentPath
{
var path :LineSegmentPath = new LineSegmentPath();
path.addNode(p1.x, p1.y, DirectionCodes.NORTH);
path.addNode(p2.x, p2.y, dir);
return path;
}
/**
* Constructs a line segment path with the specified list of points.
* An arbitrary direction will be assigned to the starting node.
*/
public static function createWithList (points :Array) :LineSegmentPath
{
var path :LineSegmentPath = new LineSegmentPath();
path.createPath(points);
return path;
}
/**
* Returns the orientation the sprite will face at the end of the
* path.
*/
public function getFinalOrientation () :int
{
return (_nodes.length == 0) ? DirectionCodes.NORTH : _nodes[_nodes.length-1].dir;
}
/**
* Add a node to the path with the specified destination point and
* facing direction.
*
* @param x the x-position.
* @param y the y-position.
* @param dir the facing direction.
*/
public function addNode (x :Number, y :Number, dir :int) :void
{
_nodes.push(new PathNode(x, y, dir));
}
/**
* Return the requested node index in the path, or null if no such
* index exists.
*
* @param idx the node index.
*
* @return the path node.
*/
public function getNode (idx :int) :PathNode
{
return _nodes[idx];
}
/**
* Return the number of nodes in the path.
*/
public function size () :int
{
return _nodes.length;
}
/**
* Sets the velocity of this pathable in pixels per millisecond. The
* velocity is measured as pixels traversed along the path that the
* pathable is traveling rather than in the x or y directions
* individually. Note that the pathable velocity should not be
* changed while a path is being traversed; doing so may result in the
* pathable position changing unexpectedly.
*
* @param velocity the pathable velocity in pixels per millisecond.
*/
public function setVelocity (velocity :Number) :void
{
_vel = velocity;
}
/**
* Computes the velocity at which the pathable will need to travel
* along this path such that it will arrive at the destination in
* approximately the specified number of milliseconds. Efforts are
* taken to get the pathable there as close to the desired time as
* possible, but framerate variation may prevent it from arriving
* exactly on time.
*/
public function setDuration (millis :int) :void
{
// if we have only zero or one nodes, we don't have enough
// information to compute our velocity
var ncount :int = _nodes.length;
if (ncount < 2) {
log.warning("Requested to set duration of bogus path " +
"[path=" + this + ", duration=" + millis + "].");
return;
}
// compute the total distance along our path
var distance :Number = 0;
var start :PathNode = _nodes[0];
for (var ii :int = 1; ii < ncount; ii++) {
var end :PathNode = _nodes[ii];
distance += MathUtil.distance(start.loc.x, start.loc.y, end.loc.x, end.loc.y);
start = end;
}
// set the velocity accordingly
setVelocity(distance/millis);
}
// documentation inherited
public function init (pable :Pathable, timestamp :int) :void
{
// give the pathable a chance to perform any starting antics
pable.pathBeginning();
// if we have only one node then let the pathable know that we're
// done straight away
if (size() < 2) {
// move the pathable to the location specified by the first
// node (assuming we have a first node)
if (size() == 1) {
var node :PathNode = _nodes[0];
pable.setLocation(node.loc.x, node.loc.y);
}
// and let the pathable know that we're done
pable.pathCompleted(timestamp);
return;
}
// and an enumeration of the path nodes
_niter = new ArrayIterator(_nodes);
// pretend like we were previously heading to our starting position
_dest = getNextNode();
// begin traversing the path
headToNextNode(pable, timestamp, timestamp);
}
// documentation inherited
public function tick (pable :Pathable, timestamp :int) :Boolean
{
// figure out how far along this segment we should be
var msecs :int = timestamp - _nodestamp;
var travpix :Number = msecs * _vel;
var pctdone :Number = travpix / _seglength;
// if we've moved beyond the end of the path, we need to adjust
// the timestamp to determine how much time we used getting to the
// end of this node, then move to the next one
if (pctdone >= 1.0) {
var used :int = int(_seglength / _vel);
return headToNextNode(pable, _nodestamp + used, timestamp);
}
// otherwise we position the pathable along the path
var ox :Number = pable.getX();
var oy :Number = pable.getY();
var nx :Number = _src.loc.x + (_dest.loc.x - _src.loc.x) * pctdone;
var ny :Number = _src.loc.y + (_dest.loc.y - _src.loc.y) * pctdone;
// Log.info("Moving pathable [msecs=" + msecs + ", pctdone=" + pctdone +
// ", travpix=" + travpix + ", seglength=" + _seglength +
// ", dx=" + (nx-ox) + ", dy=" + (ny-oy) + "].");
// only update the pathable's location if it actually moved
if (ox != nx || oy != ny) {
pable.setLocation(nx, ny);
return true;
}
return false;
}
// documentation inherited
public function fastForward (timeDelta :int) :void
{
_nodestamp += timeDelta;
}
// documentation inherited from interface
public function wasRemoved (pable :Pathable) :void
{
// nothing doing
}
/**
* Place the pathable moving along the path at the end of the previous
* path node, face it appropriately for the next node, and start it on
* its way. Returns whether the pathable position moved.
*/
protected function headToNextNode (pable :Pathable, startstamp :int, now :int) :Boolean
{
if (_niter == null) {
throw new Error("headToNextNode() called before init()");
}
// check to see if we've completed our path
if (!_niter.hasNext()) {
// move the pathable to the location of our last destination
pable.setLocation(_dest.loc.x, _dest.loc.y);
pable.pathCompleted(now);
return true;
}
// our previous destination is now our source
_src = _dest;
// pop the next node off the path
_dest = getNextNode();
// adjust the pathable's orientation
if (_dest.dir != DirectionCodes.NONE) {
pable.setOrientation(_dest.dir);
}
// make a note of when we started traversing this node
_nodestamp = startstamp;
// figure out the distance from source to destination
_seglength = MathUtil.distance(_src.loc.x, _src.loc.y, _dest.loc.x, _dest.loc.y);
// if we're already there (the segment length is zero), we skip to
// the next segment
if (_seglength == 0) {
return headToNextNode(pable, startstamp, now);
}
// now update the pathable's position based on our progress thus far
return tick(pable, now);
}
public function toString () :String
{
return StringUtil.toString(_nodes);
}
/**
* Populate the path with the path nodes that lead the pathable from
* its starting position to the given destination coordinates
* following the given list of screen coordinates.
*/
protected function createPath (points :Array) :void
{
var last :Point = null;
var size :int = points.length;
for (var ii :int = 0; ii < size; ii++) {
var p :Point = points[ii];
var dir :int = (ii == 0) ? DirectionCodes.NORTH :
DirectionUtil.getDirectionForPts(last, p);
addNode(p.x, p.y, dir);
last = p;
}
}
/**
* Gets the next node in the path.
*/
protected function getNextNode () :PathNode
{
return PathNode(_niter.next());
}
/** The nodes that make up the path. */
protected var _nodes :Array = [];
/** We use this when moving along this path. */
protected var _niter :Iterator;
/** When moving, the pathable's source path node. */
protected var _src :PathNode;
/** When moving, the pathable's destination path node. */
protected var _dest :PathNode;
/** The time at which we started traversing the current node. */
protected var _nodestamp :int;
/** The length in pixels of the current path segment. */
protected var _seglength :Number;
/** The path velocity in pixels per millisecond. */
protected var _vel :Number = DEFAULT_VELOCITY;
/** When moving, the pathable position including fractional pixels. */
protected var _movex :Number;
protected var _movey :Number;
/** When moving, the distance to move on each axis per tick. */
protected var _incx :Number;
protected var _incy :Number;
/** The distance to move on the straight path line per tick. */
protected var _fracx :Number;
protected var _fracy :Number;
/** Default pathable velocity. */
protected static const DEFAULT_VELOCITY :Number= 0.2;
}
}
@@ -0,0 +1,71 @@
//
// $Id$
//
// Nenya library - tools for developing networked games
// Copyright (C) 2002-2012 Three Rings Design, Inc., All Rights Reserved
// http://code.google.com/p/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.media.util {
/**
* A path is used to cause a {@link Pathable} to follow a particular path along the screen. The
* {@link Pathable} is responsible for calling {@link #tick} on the path with reasonable frequency
* (generally as a part of the frame tick. The path is responsible for updating the position of
* the {@link Pathable} based on the time that has elapsed since the {@link Pathable} started down
* the path.
*
* <p> The path should call the appropriate callbacks on the {@link Pathable} when appropriate
* (e.g. {@link Pathable#pathBeginning}, {@link Pathable#pathCompleted}).
*/
public interface Path
{
/**
* Called once to let the path prepare itself for the process of animating the supplied
* pathable. Path users should also call {@link #tick} after {@link #init} with the same
* initialization timestamp.
*/
function init (pable :Pathable, tickStamp :int) :void;
/**
* Called to request that this path update the position of the specified pathable based on the
* supplied timestamp information. A path should record its initial timestamp and determine
* the progress of the pathable along the path based on the time elapsed since the pathable
* began down the path.
*
* @param pable the pathable whose position should be updated.
* @param tickStamp the timestamp associated with this frame.
*
* @return true if the pathable's position was updated, false if the path determined that the
* pathable should not move at this time.
*/
function tick (pable :Pathable, tickStamp :int) :Boolean;
/**
* This is called if the pathable is paused for some length of time and then unpaused. Paths
* should adjust any time stamps they are maintaining internally by the delta so that time
* maintains the illusion of flowing smoothly forward.
*/
function fastForward (timeDelta :int) :void;
/**
* When a path is removed from a pathable, whether that is because the path was completed or
* because it was replaced by another path, this method will be called to let the path know
* that it is no longer associated with this pathable.
*/
function wasRemoved (pable :Pathable) :void;
}
}
@@ -0,0 +1,55 @@
//
// $Id$
//
// Nenya library - tools for developing networked games
// Copyright (C) 2002-2012 Three Rings Design, Inc., All Rights Reserved
// http://code.google.com/p/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.media.util {
import flash.geom.Point;
/**
* A path node is a single destination point in a {@link Path}.
*/
public class PathNode
{
/** The node coordinates in screen pixels. */
public var loc :Point;
/** The direction to face while heading toward the node. */
public var dir :int;
/**
* Construct a path node object.
*
* @param x the node x-position.
* @param y the node y-position.
* @param dir the facing direction.
*/
public function PathNode (x :Number, y :Number, dir :int)
{
loc = new Point(x, y);
this.dir = dir;
}
public function toString () :String
{
return "[x=" + loc.x + ", y=" + loc.y + ", dir=" + dir + "]";
}
}
}
@@ -0,0 +1,71 @@
//
// $Id$
//
// Nenya library - tools for developing networked games
// Copyright (C) 2002-2012 Three Rings Design, Inc., All Rights Reserved
// http://code.google.com/p/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.media.util {
import flash.geom.Rectangle;
/**
* Used in conjunction with a {@link Path}.
*/
public interface Pathable
{
/**
* Returns the pathable's current x coordinate.
*/
function getX () :Number;
/**
* Returns the pathable's current y coordinate.
*/
function getY () :Number;
/**
* Updates the pathable's current coordinates.
*/
function setLocation (x :Number, y :Number) :void;
/**
* Will be called by a path when it moves the pathable in the
* specified direction. Pathables that wish to face in the direction
* they are moving can take advantage of this callback.
*
* @see DirectionCodes
*/
function setOrientation (orient :int) :void;
/**
* Should return the orientation of the pathable, or {@link
* DirectionCodes#NONE} if the pathable does not support orientation.
*/
function getOrientation () :int;
/**
* Called by a path when this pathable is made to start along a path.
*/
function pathBeginning () :void;
/**
* Called by a path when this pathable finishes moving along its path.
*/
function pathCompleted (timestamp :int) :void;
}
}
@@ -0,0 +1,36 @@
//
// $Id$
//
// Nenya library - tools for developing networked games
// Copyright (C) 2002-2012 Three Rings Design, Inc., All Rights Reserved
// http://code.google.com/p/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.media.util {
/**
* Provides traversibility information when computing paths.
*/
public interface TraversalPred
{
/**
* Requests to know if the specified traverser (which was provided in the call to
* {@link #getPath(TraversalPred,Object,int,int,int,int,int,boolean)}) can traverse the
* specified tile coordinate.
*/
function canTraverse (traverser :Object, x :int, y :int) :Boolean;
}
}
@@ -0,0 +1,46 @@
//
// $Id$
//
// Nenya library - tools for developing networked games
// Copyright (C) 2002-2012 Three Rings Design, Inc., All Rights Reserved
// http://code.google.com/p/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.miso.client {
import flash.display.DisplayObject;
import com.threerings.media.tile.Tile;
import com.threerings.miso.util.MisoSceneMetrics;
public class BaseTileIsoSprite extends TileIsoSprite
{
public function BaseTileIsoSprite (x :int, y :int, tileId :int, tile :Tile,
metrics :MisoSceneMetrics, fringe :Tile)
{
super(x, y, tileId, tile, 0, metrics, fringe);
}
public override function layout (x :int, y :int, tile :Tile) :void
{
super.layout(x, y, tile);
setSize(1, 1, VERT_OFFSET);
}
protected static const VERT_OFFSET :Number = 0.01;
}
}
@@ -0,0 +1,86 @@
//
// $Id$
//
// Nenya library - tools for developing networked games
// Copyright (C) 2002-2012 Three Rings Design, Inc., All Rights Reserved
// http://code.google.com/p/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.miso.client {
import as3isolib.geom.Pt;
import as3isolib.geom.transformations.IAxonometricTransformation;
import com.threerings.miso.util.MisoSceneMetrics;
public class MisoMetricsTransformation implements IAxonometricTransformation
{
public function MisoMetricsTransformation (metrics :MisoSceneMetrics, zFact :Number)
{
_metrics = metrics;
_zFact = zFact;
}
public function screenToSpace (screenPt :Pt) :Pt
{
var tpos :Pt = new Pt();
// determine the upper-left of the quadrant that contains our point
var zx :int = int(Math.floor(screenPt.x / _metrics.tilewid));
var zy :int = int(Math.floor(screenPt.y / _metrics.tilehei));
// these are the screen coordinates of the tile's top
var ox :int = (zx * _metrics.tilewid);
var oy :int = (zy * _metrics.tilehei);
// these are the tile coordinates
tpos.x = zy + zx;
tpos.y = zy - zx;
tpos.z = 0;
// these are the tile coordinates
tpos.x = zy + zx; tpos.y = zy - zx;
// now determine which of the four tiles our point occupies
var dx :int = screenPt.x - ox;
var dy :int = screenPt.y - oy;
if (Math.round(_metrics.slopeY * dx + _metrics.tilehei) <= dy) {
tpos.x += 1;
}
if (Math.round(_metrics.slopeX * dx) > dy) {
tpos.y -= 1;
}
return tpos;
}
public function spaceToScreen (spacePt :Pt) :Pt
{
var spos :Pt = new Pt();
spos.x = (spacePt.x - spacePt.y) * _metrics.tilehwid;
spos.y = (spacePt.x + spacePt.y) * _metrics.tilehhei - spacePt.z * _zFact;
spos.z = 0;
return spos;
}
protected var _metrics :MisoSceneMetrics;
/** Handles any vertical transformations. */
protected var _zFact :Number;
}
}
@@ -0,0 +1,732 @@
//
// $Id$
//
// Nenya library - tools for developing networked games
// Copyright (C) 2002-2012 Three Rings Design, Inc., All Rights Reserved
// http://code.google.com/p/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.miso.client {
import flash.utils.getTimer;
import flash.display.DisplayObject;
import flash.display.Sprite;
import flash.events.Event;
import flash.geom.Point;
import flash.geom.Rectangle;
import flash.text.TextField;
import flash.text.TextFieldAutoSize;
import flash.text.TextFormat;
import flash.events.MouseEvent;
import mx.core.ClassFactory;
import as3isolib.display.IsoSprite;
import as3isolib.display.primitive.IsoBox;
import as3isolib.core.IsoDisplayObject;
import as3isolib.geom.Pt;
import as3isolib.geom.IsoMath;
import as3isolib.display.scene.IsoScene;
import as3isolib.display.IsoView;
import com.threerings.crowd.client.PlaceView;
import com.threerings.crowd.data.PlaceObject;
import com.threerings.util.ClassUtil;
import com.threerings.util.DelayUtil;
import com.threerings.util.Log;
import com.threerings.util.Map;
import com.threerings.util.Maps;
import com.threerings.util.MathUtil;
import com.threerings.util.Set;
import com.threerings.util.Sets;
import com.threerings.util.StringUtil;
import com.threerings.util.maps.WeakValueMap;
import com.threerings.media.Tickable;
import com.threerings.media.tile.BaseTile;
import com.threerings.media.tile.Colorizer;
import com.threerings.media.tile.NoSuchTileSetError;
import com.threerings.media.tile.TileSet;
import com.threerings.media.tile.TileUtil;
import com.threerings.media.util.AStarPathSearch;
import com.threerings.media.util.AStarPathSearch_Stepper;
import com.threerings.media.util.LineSegmentPath;
import com.threerings.media.util.Path;
import com.threerings.media.util.Pathable;
import com.threerings.media.util.TraversalPred;
import com.threerings.miso.client.MisoMetricsTransformation;
import com.threerings.miso.client.PrioritizedSceneLayoutRenderer;
import com.threerings.miso.data.MisoSceneModel;
import com.threerings.miso.data.ObjectInfo;
import com.threerings.miso.tile.FringeTile;
import com.threerings.miso.util.MisoContext;
import com.threerings.miso.util.ObjectSet;
import com.threerings.miso.util.MisoSceneMetrics;
public class MisoScenePanel extends Sprite
implements PlaceView, TraversalPred, Tickable
{
private var log :Log = Log.getLog(MisoScenePanel);
public function MisoScenePanel (ctx :MisoContext, metrics :MisoSceneMetrics)
{
_ctx = ctx;
// Excitingly, we get to override this globally for as3isolib...
IsoMath.transformationObject = new MisoMetricsTransformation(metrics,
metrics.tilehei * 3 / 4);
_metrics = metrics;
_isoView = new IsoView();
_isoView.setSize(DEF_WIDTH, DEF_HEIGHT);
_centerer = new Centerer(_isoView);
_vbounds = new Rectangle(0, 0,
_isoView.size.x, _isoView.size.y);
_isoView.addEventListener(MouseEvent.CLICK, onClick);
_isoView.addEventListener(MouseEvent.MOUSE_MOVE, mouseMoved);
_isoView.addEventListener(MouseEvent.ROLL_OUT, mouseExited);
addObjectScenes();
addChild(_loading = createLoadingPanel());
addEventListener(Event.ADDED_TO_STAGE, addedToStage);
addEventListener(Event.REMOVED_FROM_STAGE, removedFromStage);
}
/**
* Handles Event.ADDED_TO_STAGE.
*/
protected function addedToStage (event :Event) :void
{
_ctx.getTicker().registerTickable(this);
}
/**
* Handles Event.REMOVED_FROM_STAGE.
*/
protected function removedFromStage (event :Event) :void
{
_ctx.getTicker().removeTickable(this);
}
public function tick (tickStamp :int) :void
{
_centerer.tick(tickStamp);
}
/**
* Creates whatever we want to show while we're waiting to load our exciting scene tile data.
*/
protected function createLoadingPanel () :DisplayObject
{
// By default we just stick up a basic label for this...
var loadingText :TextField = new TextField();
loadingText.textColor = 0xFFFFFF;
var loadingFmt :TextFormat = new TextFormat();
loadingFmt.size = 24;
loadingText.defaultTextFormat = loadingFmt;
loadingText.autoSize = TextFieldAutoSize.LEFT;
_loadingProgressFunc = function (progress :Number) :void {
loadingText.text = "Loading... " + int(progress*100) + "%";
loadingText.x = (_isoView.size.x - loadingText.width)/2;
loadingText.y = (_isoView.size.y - loadingText.height)/2;
};
_loadingProgressFunc(0.0);
return loadingText;
}
protected function loadingProgress (progress :Number) :void
{
if (_loadingProgressFunc != null) {
_loadingProgressFunc(progress);
}
}
public function onClick (event :MouseEvent) :void
{
handleMousePressed(_hobject, event);
}
public function mouseMoved (event :MouseEvent) :void
{
var viewPt :Point = _isoView.globalToLocal(new Point(event.stageX, event.stageY));
var x :int = event.stageX;
var y :int = event.stageY;
// give derived classes a chance to start with a hover object
var hobject :Object = computeOverHover(x, y);
// if they came up with nothing, compute the list of objects over
// which the mouse is hovering
if (hobject == null) {
var hits :Array =
_objScene.displayListChildren.filter(
function(val :Object, idx :int, arr :Array) :Boolean {
if (val is PriorityIsoDisplayObject) {
return PriorityIsoDisplayObject(val).hitTest(x, y);
} else {
return false;
}
});
hits.sort(function (v1 :PriorityIsoDisplayObject, v2 :PriorityIsoDisplayObject) :int {
// We want reverse order, highest prio first...
return v2.getPriority() - v1.getPriority();
});
if (hits.length > 0) {
hobject = hits[0];
}
}
// if the user isn't hovering over a sprite or object with an
// action, allow derived classes to provide some other hover
if (hobject == null) {
hobject = computeUnderHover(x, y);
}
changeHoverObject(hobject);
}
/**
* Gives derived classes a chance to compute a hover object that takes precedence over sprites
* and actionable objects. If this method returns non-null, no sprite or object hover
* calculations will be performed and the object returned will become the new hover object.
*/
protected function computeOverHover (mx :int, my :int) :Object
{
return null;
}
/**
* Gives derived classes a chance to compute a hover object that is used if the mouse is not
* hovering over a sprite or actionable object. If this method is called, it means that there
* are no sprites or objects under the mouse. Thus if it returns non-null, the object returned
* will become the new hover object.
*/
protected function computeUnderHover (mx :int, my :int) :Object
{
return null;
}
/**
* Change the hover object to the new object.
*/
protected function changeHoverObject (newHover :Object) :void
{
if (newHover == _hobject) {
return;
}
var oldHover :Object = _hobject;
_hobject = newHover;
hoverObjectChanged(oldHover, newHover);
}
/**
* A place for subclasses to react to the hover object changing.
* One of the supplied arguments may be null.
*/
protected function hoverObjectChanged (oldHover :Object, newHover :Object) :void
{
// Nothing by default.
}
public function mouseExited (event :MouseEvent) :void
{
// clear the highlight tracking data
changeHoverObject(null);
}
public function handleMousePressed (hobject :Object, event :MouseEvent) :Boolean
{
var viewPt :Point = _isoView.globalToLocal(new Point(event.stageX, event.stageY));
moveBy(new Point(viewPt.x - _isoView.width / 2, viewPt.y - _isoView.height / 2), false);
return true;
}
public function moveBy (pt :Point, immediate :Boolean) :void
{
// No scene model yet, just do it...
if (_model == null) {
_centerer.moveTo(pt.x + _isoView.currentX, pt.y + _isoView.currentY, immediate);
return;
}
if (_pendingMoveBy != null) {
log.info("Already performing a move...");
return;
}
_pendingMoveBy = pt;
refreshBaseBlockScenes();
}
public function setSceneModel (model :MisoSceneModel) :void
{
_model = model;
_ctx.getTileManager().ensureLoaded(_model.getAllTilesets(), function() :void {
DelayUtil.delayFrame(function() :void {
refreshScene();
DelayUtil.delayFrame(function() :void {
removeChild(_loading);
addChild(_isoView);
});
});
}, loadingProgress);
}
public function willEnterPlace (plobj :PlaceObject) :void
{
}
public function didLeavePlace (plobj :PlaceObject) :void
{
}
/** Computes the fringe tile for the specified coordinate. */
public function computeFringeTile (tx :int, ty :int) :BaseTile
{
return _ctx.getTileManager().getFringer().getFringeTile(_model, tx, ty, _fringes,
_masks);
}
protected function getBaseBlocks (buffer :Boolean) :Set
{
var blocks :Set = Sets.newSetOf(int);
var size :Point = _isoView.size;
var xMove :int = (_pendingMoveBy == null ? 0 : _pendingMoveBy.x);
var yMove :int = (_pendingMoveBy == null ? 0 : _pendingMoveBy.y);
var minX :int = xMove - (buffer ? size.x/2 : 0);
var maxX :int = size.x + xMove + (buffer ? size.x/2 : 0);
var minY :int = yMove - (buffer ? size.y/2 : 0);
var maxY :int = BOTTOM_BUFFER + size.y + yMove + (buffer ? size.y/2 : 0);
var topLeft :Point = _isoView.localToIso(new Point(minX, minY));
var topRight :Point = _isoView.localToIso(new Point(maxX, minY));
var btmLeft :Point = _isoView.localToIso(new Point(minX, maxY));
var btmRight :Point = _isoView.localToIso(new Point(maxX, maxY));
for (var yy :int = topRight.y - 1; yy <= btmLeft.y + 1; yy++) {
for (var xx :int = topLeft.x - 1; xx <= btmRight.x + 1; xx++) {
// Toss out any that aren't actually in our view.
var blkTop :Point = _isoView.isoToLocal(new Pt(xx, yy, 0));
var blkRight :Point =
_isoView.isoToLocal(new Pt(xx + SceneBlock.BLOCK_SIZE, yy, 0));
var blkLeft :Point =
_isoView.isoToLocal(new Pt(xx, yy + SceneBlock.BLOCK_SIZE, 0));
var blkBtm :Point =
_isoView.isoToLocal(new Pt(xx + SceneBlock.BLOCK_SIZE,
yy + SceneBlock.BLOCK_SIZE, 0));
if (blkTop.y < maxY &&
blkBtm.y > minY &&
blkLeft.x < maxX &&
blkRight.x > minX) {
blocks.add(SceneBlock.getBlockKey(xx, yy));
}
}
}
return blocks;
}
protected function createSceneBlock (blockKey :int) :SceneBlock
{
return new SceneBlock(blockKey, _objScene, _isoView, _metrics);
}
protected function refreshBaseBlockScenes () :void
{
_resStartTime = getTimer();
// Clear em til we're done resolving.
_pendingPrefetchBlocks = [];
var blocks :Set = getBaseBlocks(false);
// Keep this to use in our function...
var thisRef :MisoScenePanel = this;
// Postpone calling complete til they're all queued up.
_skipComplete = true;
blocks.forEach(function(blockKey :int) :void {
if (!_blocks.containsKey(blockKey)) {
if (_prefetchedBlocks.containsKey(blockKey)) {
_readyBlocks.add(_prefetchedBlocks.remove(blockKey));
} else {
var sceneBlock :SceneBlock = createSceneBlock(blockKey);
_pendingBlocks.add(sceneBlock);
sceneBlock.resolve(_ctx, _model, thisRef, blockResolved);
}
}
});
_skipComplete = false;
if (_pendingBlocks.size() == 0) {
resolutionComplete();
}
}
protected function blockResolved (resolved :SceneBlock) :void
{
if (!_pendingBlocks.contains(resolved)) {
log.info("Trying to resolve non-pending block???: " + resolved.getKey());
}
// Move that guy from pending to ready...
_readyBlocks.add(resolved);
_pendingBlocks.remove(resolved);
if (_pendingBlocks.size() == 0 && !_skipComplete) {
resolutionComplete();
}
}
protected function renderObjectScenes () :void
{
_objScene.render();
}
protected function addObjectScenes () :void
{
_objScene = new IsoScene();
_objScene.layoutRenderer = new ClassFactory(PrioritizedSceneLayoutRenderer);
_isoView.addScene(_objScene);
}
protected function resolutionComplete () :void
{
// First, we add in our new blocks...
for each (var newBlock :SceneBlock in _readyBlocks.toArray()) {
newBlock.render();
newBlock.addToCovered(_covered);
_blocks.put(newBlock.getKey(), newBlock);
}
_readyBlocks = Sets.newSetOf(SceneBlock);
renderObjectScenes();
// Then we let the scene finally move if it's trying to...
if (_pendingMoveBy != null) {
_centerer.moveTo(_pendingMoveBy.x + _isoView.currentX,
_pendingMoveBy.y + _isoView.currentY, false);
_pendingMoveBy = null;
}
// Now, take out any old blocks no longer in our valid blocks.
var blocks :Set = getBaseBlocks(true);
for each (var baseKey :int in _blocks.keys()) {
if (!blocks.contains(baseKey)) {
_blocks.remove(baseKey).release();
}
}
// And any prefetch blocks that are bogus can go away too.
for each (var preKey :int in _prefetchedBlocks.keys()) {
if (!blocks.contains(preKey)) {
_prefetchedBlocks.remove(preKey);
}
}
log.info("Scene Block Resolution took: " + (getTimer() - _resStartTime) + "ms");
// Let's setup some prefetching...
blocks.forEach(function(blockKey :int) :void {
if (!_blocks.containsKey(blockKey)) {
var sceneBlock :SceneBlock = createSceneBlock(blockKey);
_pendingPrefetchBlocks.push(sceneBlock);
}
});
maybePrefetchABlock();
}
protected function maybePrefetchABlock () :void
{
if (_pendingPrefetchBlocks.length != 0) {
_pendingPrefetchBlocks.shift().resolve(_ctx, _model, this, prefetchBlockResolved);
}
}
protected function prefetchBlockResolved (resolved :SceneBlock) :void
{
_prefetchedBlocks.put(resolved.getKey(), resolved);
// If we haven't moved on to resolve a new region, let's try another block.
if (_pendingBlocks.size() == 0) {
DelayUtil.delayFrame(function() :void {
maybePrefetchABlock();
});
}
}
protected function refreshScene () :void
{
// Clear it out...
_isoView.removeAllScenes();
refreshBaseBlockScenes();
}
/**
* Derived classes can override this method and provide a colorizer that will be used to
* colorize the supplied scene object when rendering.
*/
public function getColorizer (oinfo :ObjectInfo) :Colorizer
{
return null;
}
// documentation inherited
public function canTraverse (traverser :Object, tx :int, ty :int) :Boolean
{
var block :SceneBlock = _blocks.get(SceneBlock.getBlockKey(tx, ty));
var baseTraversable :Boolean = (block == null) ? canTraverseUnresolved(traverser, tx, ty) :
block.canTraverseBase(traverser, tx, ty);
return baseTraversable && !_covered.contains(StringUtil.toCoordsString(tx, ty));
}
/**
* Derived classes can control whether or not we consider unresolved tiles to be traversable
* or not.
*/
protected function canTraverseUnresolved (traverser :Object, tx :int, ty :int) :Boolean
{
return false;
}
/**
* Computes a path for the specified sprite to the specified tile coordinates.
*
* @param loose if true, an approximate path will be returned if a complete path cannot be
* located. This path will navigate the sprite "legally" as far as possible and then walk the
* sprite in a straight line to its final destination. This is generally only useful if the
* the path goes "off screen".
*/
public function getPath (sprite :IsoDisplayObject, x :int, y :int, loose :Boolean) :Path
{
// sanity check
if (sprite == null) {
throw new Error("Can't get path for null sprite [x=" + x + ", y=" + y + ".");
}
// compute our longest path from the screen size
var longestPath :int = 3 * (width / _metrics.tilewid);
// get a reasonable tile path through the scene
var start :int = getTimer();
var search :AStarPathSearch = new AStarPathSearch(this, new AStarPathSearch_Stepper());
var points :Array = search.getPath(sprite, longestPath, int(Math.round(sprite.x)),
int(Math.round(sprite.y)), x, y, loose);
// Replace the starting point with the Number values rather than the rounded version...
if (points != null) {
points[0] = new Point(sprite.x, sprite.y);
}
var duration :int = getTimer() - start;
// sanity check the number of nodes searched so that we can keep an eye out for bogosity
if (duration > 500) {
log.warning("Considered a lot of nodes for path from " +
StringUtil.toCoordsString(sprite.x, sprite.y) + " to " +
StringUtil.toCoordsString(x, y) +
" [duration=" + duration + "].");
}
// construct a path object to guide the sprite on its merry way
return (points == null) ? null : LineSegmentPath.createWithList(points);
}
protected var _model :MisoSceneModel;
protected var _isoView :IsoView;
protected var _ctx :MisoContext;
protected var _metrics :MisoSceneMetrics;
/** What we display while we're loading up our tilesets. */
protected var _loading :DisplayObject;
/** If we should do something when we hear about progress updates, this is it. */
protected var _loadingProgressFunc :Function;
protected var _objScene :IsoScene;
/** All of the active blocks that are part of the scene. */
protected var _blocks :Map = Maps.newMapOf(int);
/** If we're resolving in preparation for moving, this is how much we'll move by when ready. */
protected var _pendingMoveBy :Point;
/** Required blocks we're working on resolving. */
protected var _pendingBlocks :Set = Sets.newSetOf(SceneBlock);
/** Blocks that are resolved and ready for adding to the scene. */
protected var _readyBlocks :Set = Sets.newSetOf(SceneBlock);
/** The queue of blocks we'd like to prefetch when we have a chance. */
protected var _pendingPrefetchBlocks :Array = [];
/** The blocks already prefetched and ready to go if we need them. */
protected var _prefetchedBlocks :Map = Maps.newMapOf(int);
protected var _masks :Map = Maps.newMapOf(int);
protected var _fringes :Map = new WeakValueMap(Maps.newMapOf(FringeTile));
/** What time did we start the current scene resolution. */
protected var _resStartTime :int;
/** If any block happens to resolve should we currently skip calling completion. */
protected var _skipComplete :Boolean
/** Info on the object that the mouse is currently hovering over. */
protected var _hobject :Object;
protected var _vbounds :Rectangle;
protected var _covered :Set = Sets.newSetOf(String);
protected var _centerer :Centerer;
protected const DEF_WIDTH :int = 985;
protected const DEF_HEIGHT :int = 560;
protected const BOTTOM_BUFFER :int = 300;
}
}
import as3isolib.display.IsoView;
import as3isolib.geom.Pt;
import com.threerings.media.util.LineSegmentPath;
import com.threerings.media.util.Path;
import com.threerings.media.util.Pathable;
import com.threerings.media.Tickable;
import com.threerings.util.DirectionCodes;
class Centerer
implements Pathable, Tickable
{
public function Centerer (isoView :IsoView)
{
_isoView = isoView;
}
public function moveTo (x :int, y:int, immediate :Boolean) :void
{
if (immediate) {
cancelMove();
setLocation(x, y);
} else {
var path :LineSegmentPath =
LineSegmentPath.createWithInts(_isoView.currentX, _isoView.currentY, x, y);
path.setVelocity(SCROLL_VELOCITY);
move(path);
}
}
public function move (path :Path) :void
{
// if there's a previous path, let it know that it's going away
cancelMove();
// save off this path
_path = path;
// we'll initialize it on our next tick thanks to a zero path stamp
_pathStamp = 0;
}
public function cancelMove () :void
{
if (_path != null) {
var oldpath :Path = _path;
_path = null;
oldpath.wasRemoved(this);
}
}
public function tick (tickStamp :int) :void
{
if (_path != null) {
if (_pathStamp == 0) {
_pathStamp = tickStamp
_path.init(this, _pathStamp);
}
_path.tick(this, tickStamp);
}
}
public function getX () :Number
{
return _isoView.currentX;
}
public function getY () :Number
{
return _isoView.currentY;
}
public function setLocation (x :Number, y :Number) :void
{
_isoView.centerOnPt(new Pt(x, y), false);
}
public function setOrientation (orient :int) :void
{
}
public function getOrientation () :int
{
return DirectionCodes.NONE;
}
public function pathBeginning () :void
{
}
public function pathCompleted (timestamp :int) :void
{
_path = null;
}
protected var _isoView :IsoView;
protected var _path :Path;
protected var _pathStamp :int;
/** Our scroll path velocity. */
protected static const SCROLL_VELOCITY :Number = 0.4;
}
@@ -0,0 +1,45 @@
//
// $Id$
//
// Nenya library - tools for developing networked games
// Copyright (C) 2002-2012 Three Rings Design, Inc., All Rights Reserved
// http://code.google.com/p/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.miso.client {
import com.threerings.media.tile.ObjectTile;
import com.threerings.media.tile.Tile;
import com.threerings.miso.util.MisoSceneMetrics;
public class ObjectTileIsoSprite extends TileIsoSprite
{
public function ObjectTileIsoSprite (x :int, y :int, tileId :int, tile :Tile,
priority :int, metrics :MisoSceneMetrics)
{
super(x, y, tileId, tile, ObjectTile(tile).getPriority() == 0 ?
priority : ObjectTile(tile).getPriority(), metrics);
}
public override function layout (x :int, y :int, tile :Tile) :void
{
super.layout(x, y, tile);
setSize(tile.getBaseWidth(), tile.getBaseHeight(), 1);
moveBy(-(tile.getBaseWidth() - 1), -(tile.getBaseHeight() - 1), 0);
}
}
}
@@ -0,0 +1,178 @@
//
// $Id$
//
// Nenya library - tools for developing networked games
// Copyright (C) 2002-2012 Three Rings Design, Inc., All Rights Reserved
// http://code.google.com/p/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.miso.client {
import flash.utils.Dictionary;
import flash.utils.getTimer;
import as3isolib.display.renderers.ISceneLayoutRenderer;
import as3isolib.bounds.IBounds;
import as3isolib.core.as3isolib_internal;
import as3isolib.core.IsoDisplayObject;
import as3isolib.display.scene.IIsoScene;
/**
* Adapted from the as3isolib DefaultSceneLayoutRenderer, but thanks to its private vars, I cannot
* simply extend that class.
*/
public class PrioritizedSceneLayoutRenderer
implements ISceneLayoutRenderer
{
/**
* Places all of our children in their appropriate rendering order based on their iso bounds
* and as necessary, by priority. Priority only comes into play if the iso bounds overlap.
*/
public function renderScene (scene:IIsoScene):void
{
// This is a little brute-force. Some suggestions from DefaultSceneLayoutRenderer:
// - cache dependencies between frames, only adjust invalidated objects, keeping old
// ordering as best as possible
// - screen space subdivision to limit dependency scan
// - set the invalidated children first, then do a rescan to make sure everything else is
// where it needs to be, too? probably need to order the invalidated children sets from
// low to high index
// Reset everything for this rendering pass.
_scene = scene;
_dependencies = new Dictionary();
_depth = 0;
_visited = new Dictionary();
var startTime:uint = getTimer();
// Use the non-rearranging display list so that the dependency sort will tend to
// create similar output each pass
var children:Array = _scene.displayListChildren;
// Full naive cartesian scan, see what objects are behind/in front of child[ii]
var max:uint = children.length;
for (var ii :uint = 0; ii < max; ii++)
{
var objA:IsoDisplayObject = children[ii];
var rightA:Number = objA.x + objA.width;
var frontA:Number = objA.y + objA.length;
var topA:Number = objA.z + objA.height;
var prioA :int = objA is PriorityIsoDisplayObject ?
PriorityIsoDisplayObject(objA).getPriority() : 0;
for (var jj :uint = ii + 1; jj < max; jj++)
{
var objB:IsoDisplayObject = children[jj];
var prioB :int = objB is PriorityIsoDisplayObject ?
PriorityIsoDisplayObject(objB).getPriority() : 0;
var rightB:Number = objB.x + objB.width;
var frontB:Number = objB.y + objB.length;
var topB:Number = objB.z + objB.height;
// See if B should go behind A and vice-versa.
var bBehindA :Boolean = ((objB.x < rightA) &&
(objB.y < frontA) &&
(objB.z < topA));
var aBehindB :Boolean = ((objA.x < rightB) &&
(objA.y < frontB) &&
(objA.z < topB));
if ((bBehindA && aBehindB)) {
// Overlap means we need to use the priority...
if (prioA > prioB) {
addDependency(objA, objB);
} else {
addDependency(objB, objA);
}
} else if (bBehindA) {
addDependency(objA, objB);
} else if (aBehindB) {
addDependency(objB, objA);
}
// Note - if we find that neither is behind the other, we don't add a dependency
// ordering. If they actually do visually overlap due to sticking outside their
// bounds, this can sometimes cause inconsistent visuals.
}
}
//trace("dependency scan time", getTimer() - startTime, "ms");
// Set the childrens' depth, using dependency ordering
for each (var obj:IsoDisplayObject in children) {
if (!_visited[obj]) {
place(obj);
}
}
// Clear these out so we're not retaining memory between calls
_visited = null;
_dependencies = null;
//trace("scene layout render time", getTimer() - startTime, "ms (manual sort)");
}
/**
* Adds the front-to-back dependency to our map of all such dependencies.
*/
protected function addDependency (front :IsoDisplayObject, back :IsoDisplayObject) :void
{
var deps :Array = _dependencies[front];
if (deps == null) {
deps = [];
_dependencies[front] = deps;
}
deps.push(back);
}
/**
* Dependency-ordered depth placement of the given objects and its dependencies.
*/
protected function place (obj:IsoDisplayObject):void
{
_visited[obj] = true;
for each (var inner:IsoDisplayObject in _dependencies[obj]) {
if (!_visited[inner]) {
place(inner);
}
}
if (_depth != obj.depth)
{
_scene.setChildIndex(obj, _depth);
}
_depth++;
};
/** The depth we're currently placing children at. */
protected var _depth :uint;
/** Any objects we've already placed. */
protected var _visited :Dictionary;
/** The scene on which we're operating. */
protected var _scene :IIsoScene;
/** All the front-back dependencies we know about. This maps an object to a list of all
* the objects that should be rendered behind (and therefore before) it. */
protected var _dependencies :Dictionary;
}
}
@@ -0,0 +1,39 @@
//
// $Id$
//
// Nenya library - tools for developing networked games
// Copyright (C) 2002-2012 Three Rings Design, Inc., All Rights Reserved
// http://code.google.com/p/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.miso.client {
/**
* Any Iso object wishing to specify a render priority should do so by implementing this interface.
*/
public interface PriorityIsoDisplayObject
{
/**
* Returns the render priority for the display object.
*/
function getPriority () :int;
/**
* Returns whether the location on the object should register a hit if clicked.
*/
function hitTest (stageX :int, stageY :int) :Boolean;
}
}
@@ -0,0 +1,300 @@
//
// $Id$
//
// Nenya library - tools for developing networked games
// Copyright (C) 2002-2012 Three Rings Design, Inc., All Rights Reserved
// http://code.google.com/p/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.miso.client {
import flash.geom.Point;
import flash.geom.Rectangle;
import as3isolib.display.scene.IsoScene;
import as3isolib.display.IsoView;
import com.threerings.util.Log;
import com.threerings.util.MathUtil;
import com.threerings.util.Set;
import com.threerings.util.StringUtil;
import com.threerings.media.tile.BaseTile;
import com.threerings.media.tile.Colorizer;
import com.threerings.media.tile.NoSuchTileSetError;
import com.threerings.media.tile.Tile;
import com.threerings.media.tile.TileSet;
import com.threerings.media.tile.TileUtil;
import com.threerings.miso.client.BaseTileIsoSprite;
import com.threerings.miso.client.MisoScenePanel;
import com.threerings.miso.client.ObjectTileIsoSprite;
import com.threerings.miso.data.MisoSceneModel;
import com.threerings.miso.data.ObjectInfo;
import com.threerings.miso.util.MisoContext;
import com.threerings.miso.util.MisoSceneMetrics;
import com.threerings.miso.util.ObjectSet;
public class SceneBlock
{
private var log :Log = Log.getLog(SceneBlock);
public static const BLOCK_SIZE :int = 4;
public function SceneBlock (key :int, objScene :IsoScene, isoView :IsoView,
metrics :MisoSceneMetrics)
{
_key = key;
_objScene = objScene;
_isoView = isoView;
_metrics = metrics;
}
public function getKey () :int
{
return _key;
}
public static function getBlockKey (x :int, y :int) :int
{
return (MathUtil.floorDiv(x, BLOCK_SIZE) << 16 |
(MathUtil.floorDiv(y, BLOCK_SIZE) & 0xFFFF));
}
public static function getBlockX (key :int) :int
{
return (key >> 16) * BLOCK_SIZE;
}
public static function getBlockY (key :int) :int
{
// We really do mean to do this crazy shift left then back right thing to get our sign
// back from before we encoded.
return (((key & 0xFFFF) * BLOCK_SIZE) << 16) >> 16;
}
/**
* Starts getting all the tiles for the block loaded up and ready to go.
*/
public function resolve (ctx :MisoContext, model :MisoSceneModel, panel :MisoScenePanel,
completeCallback :Function) :void
{
_completeCallback = completeCallback;
var x :int = getBlockX(_key);
var y :int = getBlockY(_key);
_baseScene = new IsoScene();
_baseScene.layoutEnabled = false;
for (var ii :int = x; ii < x + BLOCK_SIZE; ii++) {
for (var jj :int = y; jj < y + BLOCK_SIZE; jj++) {
var tileId :int = model.getBaseTileId(ii, jj);
if (tileId <= 0) {
var defSet :TileSet;
try {
var setId :int = model.getDefaultBaseTileSet();
defSet = ctx.getTileManager().getTileSet(setId);
tileId = TileUtil.getFQTileId(setId,
TileUtil.getTileHash(ii, jj) % defSet.getTileCount());
} catch (err :NoSuchTileSetError) {
// Someone else already complained...
continue;
}
}
var tileSet :TileSet;
try {
tileSet =
ctx.getTileManager().getTileSet(TileUtil.getTileSetId(tileId));
} catch (err :NoSuchTileSetError) {
// Someone else already complained...
continue;
}
if (tileSet == null) {
log.warning("TileManager returned null tilset: " +
TileUtil.getTileSetId(tileId));
continue;
}
createBaseSprite(tileSet, ii, jj, tileId, panel);
}
}
// And now grab the appropriate objects.
var set :ObjectSet = new ObjectSet();
model.getObjects(new Rectangle(x, y, BLOCK_SIZE, BLOCK_SIZE), set);
_objSprites = [];
for (ii = 0; ii < set.size(); ii++) {
var objInfo :ObjectInfo = set.get(ii);
var objTileId :int = objInfo.tileId;
var objTileSet :TileSet;
try {
objTileSet =
ctx.getTileManager().getTileSet(TileUtil.getTileSetId(objTileId));
} catch (err :NoSuchTileSetError) {
// Someone else already complained...
continue;
}
if (objTileSet == null) {
log.warning("TileManager returned null TileSet: " +
TileUtil.getTileSetId(objTileId));
continue;
}
createObjSprite(objTileSet, objInfo, panel);
}
maybeLoaded();
}
/**
* Actually adds all our tiles as sprites to the scenes.
*/
public function render () :void
{
_isoView.addSceneAt(_baseScene, 0);
_baseScene.render();
for each (var sprite :ObjectTileIsoSprite in _objSprites) {
_objScene.addChild(sprite);
}
}
public function addToCovered (covered :Set) :void
{
for each (var sprite :ObjectTileIsoSprite in _objSprites) {
for (var xx :int = sprite.x; xx < sprite.x + sprite.width; xx++) {
for (var yy :int = sprite.y; yy < sprite.y + sprite.length; yy++) {
covered.add(StringUtil.toCoordsString(xx, yy));
}
}
}
}
/**
* Removes our tiles' sprites from the scenes.
*/
public function release () :void
{
_isoView.removeScene(_baseScene);
for each (var sprite :ObjectTileIsoSprite in _objSprites) {
_objScene.removeChild(sprite);
}
}
protected function createBaseSprite (tileSet :TileSet, x :int, y :int, tileId :int,
panel :MisoScenePanel) :void
{
var fringeTile :BaseTile = panel.computeFringeTile(x, y);
var tile :BaseTile = BaseTile(tileSet.getTile(TileUtil.getTileIndex(tileId)));
var bx :int = getBlockX(_key);
var by :int = getBlockY(_key);
_passable[(y - by) * BLOCK_SIZE + (x - bx)] =
tile.isPassable() && (fringeTile == null || fringeTile.isPassable());
if (tile.getImage() != null && (fringeTile == null || fringeTile.getImage() != null)) {
_baseScene.addChild(new BaseTileIsoSprite(x, y, tileId, tile, _metrics, fringeTile));
} else {
noteTileToLoad();
var maybeLoaded :Function = function (loaded :Tile) :void {
if (tile.getImage() != null &&
(fringeTile == null || fringeTile.getImage() != null)) {
_baseScene.addChild(new BaseTileIsoSprite(x, y, tileId, tile, _metrics,
fringeTile));
noteTileLoaded();
}
};
if (tile.getImage() == null) {
tile.notifyOnLoad(maybeLoaded);
}
if (fringeTile != null && fringeTile.getImage() == null) {
fringeTile.notifyOnLoad(maybeLoaded);
}
}
}
protected function createObjSprite (tileSet :TileSet, objInfo :ObjectInfo,
panel :MisoScenePanel) :void
{
var tile :Tile = tileSet.getTile(TileUtil.getTileIndex(objInfo.tileId),
panel.getColorizer(objInfo));
if (tile.getImage() != null) {
_objSprites.push(new ObjectTileIsoSprite(objInfo.x, objInfo.y, objInfo.tileId, tile,
objInfo.priority, _metrics));
} else {
noteTileToLoad();
tile.notifyOnLoad(function (tile :Tile) :void {
_objSprites.push(new ObjectTileIsoSprite(objInfo.x, objInfo.y, objInfo.tileId, tile,
objInfo.priority, _metrics));
noteTileLoaded();
});
}
}
/**
* Returns whether the base & fringe tiles are traversable here.
*/
public function canTraverseBase (traverser :Object, tx :int, ty :int) :Boolean
{
// Only handles the base tiles since the objects cross multiple scene blocks...
var bx :int = getBlockX(_key);
var by :int = getBlockY(_key);
return _passable[(ty - by) * BLOCK_SIZE + (tx - bx)];
}
protected function noteTileToLoad () :void
{
_pendingCt++;
}
protected function noteTileLoaded () :void
{
_pendingCt--;
maybeLoaded();
}
protected function maybeLoaded () :void
{
if (_pendingCt == 0) {
_completeCallback(this);
_completeCallback = null;
}
}
protected var _passable :Array = new Array(BLOCK_SIZE * BLOCK_SIZE);
protected var _baseScene :IsoScene;
protected var _objSprites :Array;
protected var _key :int;
protected var _completeCallback :Function;
protected var _pendingCt :int;
protected var _objScene :IsoScene;
protected var _isoView :IsoView;
protected var _metrics :MisoSceneMetrics;
}
}
@@ -0,0 +1,132 @@
//
// $Id$
//
// Nenya library - tools for developing networked games
// Copyright (C) 2002-2012 Three Rings Design, Inc., All Rights Reserved
// http://code.google.com/p/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.miso.client {
import flash.display.Bitmap;
import flash.display.DisplayObject;
import flash.geom.Point;
import as3isolib.core.IsoDisplayObject;
import as3isolib.display.IsoSprite;
import as3isolib.display.primitive.IsoBox;
import as3isolib.graphics.SolidColorFill;
import com.threerings.media.tile.Tile;
import com.threerings.media.tile.TileSet;
import com.threerings.media.tile.TileUtil;
import com.threerings.util.Log;
import com.threerings.miso.util.MisoSceneMetrics;
public class TileIsoSprite extends IsoSprite
implements PriorityIsoDisplayObject
{
private static var log :Log = Log.getLog(TileIsoSprite);
public function TileIsoSprite (x :int, y :int, tileId :int, tile :Tile, priority :int,
metrics :MisoSceneMetrics, fringe :Tile = null)
{
_tileId = tileId;
_metrics = metrics;
_priority = priority;
_fringe = fringe;
layout(x, y, tile);
setSize(tile.getBaseWidth(), tile.getBaseHeight(), 1);
if (tile.getImage() == null) {
tile.notifyOnLoad(function() :void {
gotTileImage(tile);
});
} else {
gotTileImage(tile);
}
}
public function layout (x :int, y :int, tile :Tile) :void
{
moveTo(x, y, 0);
}
protected function gotTileImage (tile :Tile) :void
{
var image :DisplayObject = tile.getImage();
// as3isolib uses top instead of bottom.
image.x = -tile.getOriginX() + _metrics.tilewid *
((tile.getBaseWidth() - tile.getBaseHeight())/2);
image.y = -tile.getOriginY() + _metrics.tilehei *
((tile.getBaseWidth() + tile.getBaseHeight())/2);
if (_fringe == null) {
sprites = [image];
} else {
var fringeImg :DisplayObject = _fringe.getImage();
// as3isolib uses top instead of bottom.
fringeImg.x = -_fringe.getOriginX() + _metrics.tilewid *
((_fringe.getBaseWidth() - _fringe.getBaseHeight())/2);
fringeImg.y = -_fringe.getOriginY() + _metrics.tilehei *
((_fringe.getBaseWidth() + _fringe.getBaseHeight())/2);
sprites = [image, fringeImg];
}
render();
}
public function getPriority () :int
{
return _priority;
}
public function hitTest (stageX :int, stageY :int) :Boolean
{
if (sprites == null || sprites.length == 0) {
return false;
}
if (sprites[0] is Bitmap) {
if (!sprites[0].hitTestPoint(stageX, stageY, true)) {
// Doesn't even hit the bounds...
return false;
}
// Check the actual pixels...
var pt :Point = sprites[0].globalToLocal(new Point(stageX, stageY));
return Bitmap(sprites[0]).bitmapData.hitTest(new Point(0, 0), 0, pt);
} else {
return sprites[0].hitTestPoint(stageX, stageY, true);
}
}
public function toString () :String
{
return x + ", " + y + ": " + _tileId;
}
protected var _tileId :int;
protected var _priority :int;
protected var _fringe :Tile;
protected var _metrics :MisoSceneMetrics;
}
}
@@ -0,0 +1,106 @@
//
// $Id$
//
// Nenya library - tools for developing networked games
// Copyright (C) 2002-2012 Three Rings Design, Inc., All Rights Reserved
// http://code.google.com/p/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.miso.data {
import flash.geom.Rectangle;
import com.threerings.miso.util.ObjectSet;
import com.threerings.io.SimpleStreamableObject;
import com.threerings.miso.data.MisoSceneModel;
import com.threerings.io.ObjectInputStream;
import com.threerings.miso.data.ObjectInfo;
import com.threerings.util.ClassUtil;
import com.threerings.util.Cloneable;
import com.threerings.util.Set;
import com.threerings.io.ObjectOutputStream;
/**
* Contains basic information for a miso scene model that is shared among
* the specialized model implementations.
*/
public /*abstract*/ class MisoSceneModel extends SimpleStreamableObject
implements Cloneable
{
public function clone () :Object
{
return (ClassUtil.newInstance(this) as MisoSceneModel);
}
/**
* Returns the fully qualified tile id of the base tile at the
* specified coordinates. <code>-1</code> will be returned if there is
* no tile at the specified coordinate.
*/
public function getBaseTileId (x :int, y :int) :int
{
throw new Error("abstract");
}
public function setBaseTile (x :int, y :int, tileId :int) :Boolean
{
throw new Error("abstract");
}
public function setDefaultBaseTileSet (tileSetId :int) :void
{
// nothing doing
}
/**
* Scene models can return a default tileset to be used when no base
* tile data exists for a particular tile.
*/
public function getDefaultBaseTileSet () :int
{
return 0;
}
/**
* Populates the supplied object set with info on all objects whose
* origin falls in the requested region.
*/
public function getObjects (region :Rectangle, set :ObjectSet) :void
{
throw new Error("abstract");
}
public function addObject (info :ObjectInfo) :Boolean
{
throw new Error("abstract");
}
public function updateObject (info :ObjectInfo) :void
{
throw new Error("abstract");
}
public function removeObject (info :ObjectInfo) :Boolean
{
throw new Error("abstract");
}
public function getAllTilesets () :Set
{
throw new Error("abstract");
}
}
}
@@ -0,0 +1,201 @@
//
// $Id$
//
// Nenya library - tools for developing networked games
// Copyright (C) 2002-2012 Three Rings Design, Inc., All Rights Reserved
// http://code.google.com/p/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.miso.data {
import com.threerings.util.Hashable;
import com.threerings.util.ClassUtil;
import com.threerings.util.Cloneable;
import com.threerings.util.StringUtil;
import com.threerings.io.SimpleStreamableObject;
import com.threerings.io.ObjectInputStream;
import com.threerings.io.ObjectOutputStream;
import com.threerings.miso.data.ObjectInfo;
import com.threerings.media.tile.TileUtil;
/**
* Contains information about an object in a Miso scene.
*/
public class ObjectInfo extends SimpleStreamableObject
implements Cloneable, Hashable
{
/** The fully qualified object tile id. */
public var tileId :int;
/** The x and y tile coordinates of the object. */
public var x :int;
public var y :int;
/** Don't access this directly unless you are serializing this
* instance. Use {@link #getPriority} instead. */
public var priority :int = 0;
/** The action associated with this object or null if it has no
* action. */
public var action :String;
/** A "spot" associated with this object (specified as an offset from
* the fine coordinates of the object's origin tile). */
public var sx :int;
public var sy :int;
/** The orientation of the "spot" associated with this object. */
public var sorient :int;
/** Up to two colorization assignments for this object. */
public var zations :int;
public function ObjectInfo (tileId :int = 0, x :int = 0, y :int = 0)
{
this.tileId = tileId;
this.x = x;
this.y = y;
}
public function hashCode () :int
{
return x ^ y ^ tileId;
}
public function clone () :Object
{
var info :ObjectInfo = (ClassUtil.newInstance(this) as ObjectInfo);
info.tileId = tileId;
info.x = x;
info.y = y;
info.priority = priority;
info.action = action;
info.sx = sx;
info.sy = sy;
info.sorient = sorient;
info.zations = zations;
return info;
}
public function equals (other :Object) :Boolean
{
if (other is ObjectInfo) {
var ooi :ObjectInfo = ObjectInfo(other);
return (x == ooi.x && y == ooi.y && tileId == ooi.tileId);
} else {
return false;
}
}
/**
* Returns the render priority of this object tile.
*/
public function getPriority () :int
{
return priority;
}
/**
* Returns the primary colorization assignment.
*/
public function getPrimaryZation () :int
{
return (zations & 0xFF);
}
/**
* Returns the secondary colorization assignment.
*/
public function getSecondaryZation () :int
{
return ((zations >> 16) & 0xFF);
}
/**
* Returns the tertiary colorization assignment.
*/
public function getTertiaryZation () :int
{
return ((zations >> 24) & 0xFF);
}
/**
* Returns the quaternary colorization assignment.
*/
public function getQuaternaryZation () :int
{
return ((zations >> 8) & 0xFF);
}
/**
* Sets the primary and secondary colorization assignments.
*/
public function setZations (primary :int, secondary :int, tertiary :int, quaternary :int) :void
{
zations = (primary | (secondary << 16) | (tertiary << 24) | (quaternary << 8));
}
/**
* Returns true if this object info contains non-default data for
* anything other than the tile id and coordinates.
*/
public function isInteresting () :Boolean
{
return (!StringUtil.isBlank(action) || priority != 0 ||
sx != 0 || sy != 0 || zations != 0);
}
/** Enhances our {@link SimpleStreamableObject#toString} output. */
public function tileIdToString () :String
{
return (TileUtil.getTileSetId(tileId) + ":" +
TileUtil.getTileIndex(tileId));
}
// from interface Streamable
override public function readObject (ins :ObjectInputStream) :void
{
super.readObject(ins);
tileId = ins.readInt();
x = ins.readInt();
y = ins.readInt();
priority = ins.readByte();
action = ins.readField(String);
sx = ins.readByte();
sy = ins.readByte();
sorient = ins.readByte();
zations = ins.readInt();
}
// from interface Streamable
override public function writeObject (out :ObjectOutputStream) :void
{
super.writeObject(out);
out.writeInt(tileId);
out.writeInt(x);
out.writeInt(y);
out.writeByte(priority);
out.writeField(action);
out.writeByte(sx);
out.writeByte(sy);
out.writeByte(sorient);
out.writeInt(zations);
}
}
}
@@ -0,0 +1,263 @@
//
// $Id$
//
// Nenya library - tools for developing networked games
// Copyright (C) 2002-2012 Three Rings Design, Inc., All Rights Reserved
// http://code.google.com/p/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.miso.data {
import flash.geom.Rectangle;
import com.threerings.io.ObjectInputStream;
import com.threerings.io.SimpleStreamableObject;
import com.threerings.io.ObjectOutputStream;
import com.threerings.util.ArrayIterator;
import com.threerings.util.ClassUtil;
import com.threerings.util.Integer;
import com.threerings.util.Iterator;
import com.threerings.util.Joiner;
import com.threerings.util.MathUtil;
import com.threerings.util.Set;
import com.threerings.util.Sets;
import com.threerings.util.StreamableHashMap;
import com.threerings.util.StringUtil;
import com.threerings.miso.util.ObjectSet;
import com.threerings.miso.data.MisoSceneModel;
import com.threerings.miso.data.SparseMisoSceneModel;
import com.threerings.miso.data.ObjectInfo;
import com.threerings.miso.data.SparseMisoSceneModel_ObjectVisitor;
import com.threerings.miso.data.SparseMisoSceneModel_Section;
/**
* Contains miso scene data that is broken up into NxN tile sections.
*/
public class SparseMisoSceneModel extends MisoSceneModel
{
public var swidth :int;
public var sheight :int;
/** The dimensions of a section of our scene. */
/** The tileset to use when we have no tile data. */
public var defTileSet :int = 0;
override public function clone () :Object
{
var model :SparseMisoSceneModel = (ClassUtil.newInstance(this) as SparseMisoSceneModel);
model._sections = new StreamableHashMap();
for (var iter :Iterator = getSections(); iter.hasNext(); ) {
var sect :SparseMisoSceneModel_Section = SparseMisoSceneModel_Section(iter.next());
model.setSection(SparseMisoSceneModel_Section(sect.clone()));
}
return model;
}
override public function getBaseTileId (col :int, row :int) :int
{
var sec :SparseMisoSceneModel_Section = getSection(col, row, false);
return (sec == null) ? -1 : sec.getBaseTileId(col, row);
}
override public function setBaseTile (fqBaseTileId :int, col :int, row :int) :Boolean
{
getSection(col, row, true).setBaseTile(col, row, fqBaseTileId);
return true;
}
override public function setDefaultBaseTileSet (tileSetId :int) :void
{
defTileSet = tileSetId;
}
override public function getDefaultBaseTileSet () :int
{
return defTileSet;
}
override public function getObjects (region :Rectangle, set :ObjectSet) :void
{
var minx :int = MathUtil.floorDiv(region.x, swidth)*swidth;
var maxx :int = MathUtil.floorDiv(region.x+region.width-1, swidth)*swidth;
var miny :int = MathUtil.floorDiv(region.y, sheight)*sheight;
var maxy :int = MathUtil.floorDiv(region.y+region.height-1, sheight)*sheight;
for (var yy :int = miny; yy <= maxy; yy += sheight) {
for (var xx :int = minx; xx <= maxx; xx += swidth) {
var sec :SparseMisoSceneModel_Section = getSection(xx, yy, false);
if (sec != null) {
sec.getObjects(region, set);
}
}
}
}
override public function addObject (info :ObjectInfo) :Boolean
{
return getSection(info.x, info.y, true).addObject(info);
}
override public function updateObject (info :ObjectInfo) :void
{
// not efficient, but this is only done in editing situations
removeObject(info);
addObject(info);
}
override public function removeObject (info :ObjectInfo) :Boolean
{
var sec :SparseMisoSceneModel_Section = getSection(info.x, info.y, false);
if (sec != null) {
return sec.removeObject(info);
} else {
return false;
}
}
/**
* Adds all interesting {@link ObjectInfo} records in this scene to
* the supplied list.
*/
public function getInterestingObjects (list :Array) :void
{
for (var iter :Iterator = getSections(); iter.hasNext(); ) {
var sect :SparseMisoSceneModel_Section = SparseMisoSceneModel_Section(iter.next());
for each (var element :ObjectInfo in sect.objectInfo) {
list.add(element);
}
}
}
/**
* Don't call this method! This is only public so that the scene
* writer can generate XML from the raw scene data.
*/
public function getSections () :Iterator
{
return new ArrayIterator(_sections.values());
}
/**
* Adds all {@link ObjectInfo} records in this scene to the supplied list.
*/
public function getAllObjects (list :Array) :void
{
for (var iter :Iterator = getSections(); iter.hasNext(); ) {
iter.next().getAllObjects(list);
}
}
/**
* Informs the supplied visitor of each object in this scene.
*
* @param interestingOnly if true, only the interesting objects will
* be visited.
*/
public function visitObjects (visitor :SparseMisoSceneModel_ObjectVisitor,
interestingOnly :Boolean = false) :void
{
for (var iter :Iterator = getSections(); iter.hasNext(); ) {
var sect :SparseMisoSceneModel_Section = SparseMisoSceneModel_Section(iter.next());
for each (var oinfo :ObjectInfo in sect.objectInfo) {
visitor.visit(oinfo);
}
if (!interestingOnly) {
for (var oo :int = 0; oo < sect.objectTileIds.length; oo++) {
var info :ObjectInfo = new ObjectInfo(sect.objectTileIds[oo],
sect.objectXs[oo], sect.objectYs[oo]);
visitor.visit(info);
}
}
}
}
/**
* Don't call this method! This is only public so that the scene
* parser can construct a scene from raw data. If only Java supported
* class friendship.
*/
public function setSection (section :SparseMisoSceneModel_Section) :void
{
_sections.put(key(section.x, section.y), section);
}
override public function getAllTilesets () :Set
{
var tilesets :Set = Sets.newSetOf(int);
for each (var section :SparseMisoSceneModel_Section in _sections.values()) {
section.getAllTilesets(tilesets);
}
tilesets.add(getDefaultBaseTileSet());
return tilesets;
}
// from interface Streamable
override public function readObject (ins :ObjectInputStream) :void
{
super.readObject(ins);
swidth = ins.readShort();
sheight = ins.readShort();
defTileSet = ins.readInt();
_sections = ins.readObject(StreamableHashMap);
}
// from interface Streamable
override public function writeObject (out :ObjectOutputStream) :void
{
super.writeObject(out);
out.writeShort(swidth);
out.writeShort(sheight);
out.writeInt(defTileSet);
out.writeObject(_sections);
}
override protected function toStringJoiner (j :Joiner) :void
{
super.toStringJoiner(j);
j.add("sections", StringUtil.toString(_sections.values()));
}
/**
* Returns the key for the specified section.
*/
protected function key (x :int, y :int) :Integer
{
var sx :int = MathUtil.floorDiv(x, swidth);
var sy :int = MathUtil.floorDiv(y, sheight);
return Integer.valueOf((sx << 16) | (sy & 0xFFFF));
}
/** Returns the section for the specified tile coordinate. */
protected function getSection (x :int, y :int, create :Boolean) :SparseMisoSceneModel_Section
{
var key :Integer = key(x, y);
var sect :SparseMisoSceneModel_Section = _sections.get(key);
if (sect == null && create) {
var sx :int = MathUtil.floorDiv(x, swidth)*swidth;
var sy :int = MathUtil.floorDiv(y, sheight)*sheight;
_sections.put(key, sect = new SparseMisoSceneModel_Section(sx, sy, swidth, sheight));
}
return sect;
}
/** Contains our sections in row major order. */
protected var _sections :StreamableHashMap = new StreamableHashMap();
}
}
@@ -0,0 +1,29 @@
//
// $Id$
//
// Nenya library - tools for developing networked games
// Copyright (C) 2002-2012 Three Rings Design, Inc., All Rights Reserved
// http://code.google.com/p/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.miso.data {
public interface SparseMisoSceneModel_ObjectVisitor
{
/** Called for each object in the scene, interesting and not. */
function visit (info :ObjectInfo) :void;
}
}
@@ -0,0 +1,287 @@
//
// $Id$
//
// Nenya library - tools for developing networked games
// Copyright (C) 2002-2012 Three Rings Design, Inc., All Rights Reserved
// http://code.google.com/p/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.miso.data {
import flash.geom.Rectangle;
import com.threerings.io.ObjectOutputStream;
import com.threerings.io.ObjectInputStream;
import com.threerings.io.SimpleStreamableObject;
import com.threerings.io.TypedArray;
import com.threerings.media.tile.TileUtil;
import com.threerings.miso.util.ObjectSet;
import com.threerings.util.Arrays;
import com.threerings.util.ClassUtil;
import com.threerings.util.Cloneable;
import com.threerings.util.Log;
import com.threerings.util.Set;
import com.threerings.util.StringUtil;
public class SparseMisoSceneModel_Section extends SimpleStreamableObject
implements Cloneable
{
/** The tile coordinate of our upper leftmost tile. */
public var x :int;
public var y :int;
/** The width of this section in tiles. */
public var width :int;
/** The combined tile ids (tile set id and tile id) for our
* section (in row major order). */
public var baseTileIds :TypedArray;
/** The combined tile ids (tile set id and tile id) of the
* "uninteresting" tiles in the object layer. */
public var objectTileIds :TypedArray = TypedArray.create(int);
/** The x coordinate of the "uninteresting" tiles in the object
* layer. */
public var objectXs :TypedArray = TypedArray.createShort();
/** The y coordinate of the "uninteresting" tiles in the object
* layer. */
public var objectYs :TypedArray = TypedArray.createShort();
/** Information records for the "interesting" objects in the
* object layer. */
public var objectInfo :TypedArray = TypedArray.create(ObjectInfo);
/**
* Creates a new scene section with the specified dimensions.
*/
public function SparseMisoSceneModel_Section (
x :int = 0, y :int = 0, width :int = 0, height :int = 0)
{
this.x = x;
this.y = y;
this.width = width;
baseTileIds = TypedArray.create(int, Arrays.create(width*height));
}
public function getBaseTileId (col :int, row :int) :int
{
if (col < x || col >= (x+width) || row < y || row >= (y+width)) {
log.warning("Requested bogus tile +" + col + "+" + row +
" from " + this + ".");
return -1;
} else {
return baseTileIds[(row-y)*width+(col-x)];
}
}
public function setBaseTile (col :int, row :int, fqBaseTileId :int) :void
{
baseTileIds[(row-y)*width+(col-x)] = fqBaseTileId;
}
public function addObject (info :ObjectInfo) :Boolean
{
// sanity check: see if there is already an object of this
// type at these coordinates
var dupidx :int;
if ((dupidx = Arrays.indexOf(objectInfo, info)) != -1) {
log.warning("Refusing to add duplicate object [ninfo=" + info +
", oinfo=" + objectInfo[dupidx] + "].");
return false;
}
if ((dupidx = indexOfUn(info)) != -1) {
log.warning("Refusing to add duplicate object " +
"[info=" + info + "].");
return false;
}
if (info.isInteresting()) {
objectInfo = TypedArray(objectInfo.concat(info));
} else {
objectTileIds = TypedArray(objectTileIds.concat(info.tileId));
objectXs = TypedArray(objectXs.concat(info.x));
objectYs = TypedArray(objectYs.concat(info.y));
}
return true;
}
public function removeObject (info :ObjectInfo) :Boolean
{
// look for it in the interesting info array
var oidx :int = Arrays.indexOf(objectInfo, info);
if (oidx != -1) {
objectInfo = TypedArray(Arrays.splice(objectInfo, oidx, 1));
return true;
}
// look for it in the uninteresting arrays
oidx = indexOfUn(info);
if (oidx != -1) {
objectTileIds = TypedArray(Arrays.splice(objectTileIds, oidx, 1));
objectXs = TypedArray(Arrays.splice(objectXs, oidx, 1));
objectYs = TypedArray(Arrays.splice(objectYs, oidx, 1));
return true;
}
return false;
}
/**
* Returns the index of the specified object in the uninteresting
* arrays or -1 if it is not in this section as an uninteresting
* object.
*/
protected function indexOfUn (info :ObjectInfo) :int
{
for (var ii :int = 0; ii < objectTileIds.length; ii++) {
if (objectTileIds[ii] == info.tileId &&
objectXs[ii] == info.x && objectYs[ii] == info.y) {
return ii;
}
}
return -1;
}
public function getAllObjects (list :Array) :void
{
for each (var info :ObjectInfo in objectInfo) {
list.add(info);
}
for (var ii :int= 0; ii < objectTileIds.length; ii++) {
var x :int = objectXs[ii];
var y :int = objectYs[ii];
list.add(new ObjectInfo(objectTileIds[ii], x, y));
}
}
public function getObjects (region :Rectangle, set :ObjectSet) :void
{
// first look for intersecting interesting objects
for each (var info :ObjectInfo in objectInfo) {
if (region.contains(info.x, info.y)) {
set.insert(info);
}
}
// now look for intersecting non-interesting objects
for (var ii :int = 0; ii < objectTileIds.length; ii++) {
var x :int = objectXs[ii];
var y :int = objectYs[ii];
if (region.contains(x, y)) {
set.insert(new ObjectInfo(objectTileIds[ii], x, y));
}
}
}
/**
* Returns true if this section contains no data beyond the default.
* Used when saving a sparse scene: we omit blank sections.
*/
public function isBlank () :Boolean
{
if ((objectTileIds.length != 0) || (objectInfo.length != 0)) {
return false;
}
for each (var baseTileId :int in baseTileIds) {
if (baseTileId != 0) {
return false;
}
}
return true;
}
public function clone () :Object
{
var section :SparseMisoSceneModel_Section =
(ClassUtil.newInstance(this) as SparseMisoSceneModel_Section);
section.x = x;
section.y = y;
section.width = width;
section.baseTileIds = TypedArray(Arrays.copyOf(baseTileIds));
section.objectTileIds = TypedArray(Arrays.copyOf(objectTileIds));
section.objectXs = TypedArray(Arrays.copyOf(objectXs));
section.objectYs = TypedArray(Arrays.copyOf(objectYs));
section.objectInfo = TypedArray.create(ObjectInfo);
for (var ii :int = 0; ii < objectInfo.length; ii++) {
section.objectInfo[ii] = objectInfo[ii].clone();
}
return section;
}
override public function toString () :String
{
if (width == 0 || baseTileIds == null) {
return "<no bounds>";
} else {
return width + "x" + (baseTileIds.length / width) + "+" + x + ":" + y + ":" +
objectInfo.length;
}
}
/**
* Adds all tilesets we reference to the set.
*/
public function getAllTilesets (tilesets :Set) :void
{
for each (var base :int in baseTileIds) {
var baseSetId :int = TileUtil.getTileSetId(base);
if (baseSetId != 0) {
tilesets.add(baseSetId);
}
}
for each (var obj :int in objectTileIds) {
tilesets.add(TileUtil.getTileSetId(obj));
}
for each (var objInfo :ObjectInfo in objectInfo) {
tilesets.add(TileUtil.getTileSetId(objInfo.tileId));
}
}
// from interface Streamable
override public function readObject (ins :ObjectInputStream) :void
{
super.readObject(ins);
x = ins.readShort();
y = ins.readShort();
width = ins.readInt();
baseTileIds = ins.readField(TypedArray.getJavaType(int));
objectTileIds = ins.readField(TypedArray.getJavaType(int));
objectXs = ins.readField(TypedArray.getJavaShortType());
objectYs = ins.readField(TypedArray.getJavaShortType());
objectInfo = TypedArray(ins.readObject());
}
// from interface Streamable
override public function writeObject (out :ObjectOutputStream) :void
{
super.writeObject(out);
out.writeShort(x);
out.writeShort(y);
out.writeInt(width);
out.writeField(baseTileIds);
out.writeField(objectTileIds);
out.writeField(objectXs);
out.writeField(objectYs);
out.writeField(objectInfo);
}
private static const log :Log = Log.getLog(SparseMisoSceneModel_Section);
}
}
@@ -0,0 +1,56 @@
//
// $Id$
//
// Nenya library - tools for developing networked games
// Copyright (C) 2002-2012 Three Rings Design, Inc., All Rights Reserved
// http://code.google.com/p/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.miso.data {
import com.threerings.io.ObjectInputStream;
import com.threerings.miso.data.ObjectInfo;
import com.threerings.io.ObjectOutputStream;
/**
* A convenient base class for "virtual" scenes which do not allow editing
* and compute the base and object tiles rather than obtain them from some
* data structure.
*/
public /*abstract*/ class VirtualMisoSceneModel extends MisoSceneModel
{
override public function setBaseTile (arg1 :int, arg2 :int, arg3 :int) :Boolean
{
throw new Error("Unsupported");
}
override public function addObject (arg1 :ObjectInfo) :Boolean
{
throw new Error("Unsupported");
}
override public function updateObject (arg1 :ObjectInfo) :void
{
throw new Error("Unsupported");
}
override public function removeObject (arg1 :ObjectInfo) :Boolean
{
throw new Error("Unsupported");
}
}
}
@@ -0,0 +1,474 @@
//
// $Id$
//
// Nenya library - tools for developing networked games
// Copyright (C) 2002-2012 Three Rings Design, Inc., All Rights Reserved
// http://code.google.com/p/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.miso.tile {
import flash.geom.Point;
import flash.geom.Rectangle;
import flash.display.Bitmap;
import flash.display.BitmapData;
import com.threerings.display.ImageUtil;
import com.threerings.media.tile.NoSuchTileSetError;
import com.threerings.media.tile.BaseTile;
import com.threerings.media.tile.Tile;
import com.threerings.media.tile.TileSet;
import com.threerings.media.tile.TileManager;
import com.threerings.media.tile.TileUtil;
import com.threerings.util.Arrays;
import com.threerings.util.Integer;
import com.threerings.util.Log;
import com.threerings.util.Maps;
import com.threerings.util.Map;
import com.threerings.util.Sets;
import com.threerings.util.Set;
import com.threerings.util.StringUtil;
import com.threerings.util.WeakReference;
import com.threerings.miso.data.MisoSceneModel;
public class AutoFringer
{
private var log :Log = Log.getLog(AutoFringer);
public function AutoFringer (fringeConf :FringeConfiguration, tMgr :TileManager)
{
_fringeConf = fringeConf;
_tMgr = tMgr;
// Construct the BITS_TO_INDEX array.
var initIdx :int;
// first clear everything to -1 (meaning there is no tile defined)
for (initIdx = 0; initIdx < INIT_CT; initIdx++) {
BITS_TO_INDEX[initIdx] = -1;
}
// then fill in with the defined tiles.
for (initIdx = 0; initIdx < FRINGETILES.length; initIdx++) {
BITS_TO_INDEX[FRINGETILES[initIdx]] = initIdx;
}
}
/**
* Returns the fringe configuration used by this fringer.
*/
public function getFringeConf () :FringeConfiguration
{
return _fringeConf;
}
/**
* Returns all the tilesets that fringe on the specified tilesets.
*/
public function getFringeSets (tilesets :Set) :Set
{
var fringeSets :Set = Sets.newSetOf(int);
tilesets.forEach(function(o :int) :void {
var fringes :Array = _fringeConf.getFringeSets(o);
if (fringes != null) {
for each (var fringe :int in fringes) {
fringeSets.add(fringe);
}
}
});
return fringeSets;
}
/**
* Compute and return the fringe tile to be inserted at the specified location.
*/
public function getFringeTile (scene :MisoSceneModel, col :int, row :int, fringes :Map,
masks :Map) :BaseTile
{
// get the tileset id of the base tile we are considering
var utid :int = scene.getBaseTileId(col, row);
var underset :int = adjustTileSetId((utid <= 0) ?
scene.getDefaultBaseTileSet() : (utid >> 16));
// start with a clean temporary fringer map
_fringers.clear();
var passable :Boolean = true;
// walk through our influence tiles
for (var y :int = row - 1, maxy :int = row + 2; y < maxy; y++) {
for (var x :int = col - 1, maxx :int = col + 2; x < maxx; x++) {
// we sensibly do not consider ourselves
if ((x == col) && (y == row)) {
continue;
}
// determine the tileset for this tile
var btid :int= scene.getBaseTileId(x, y);
var baseset :int= adjustTileSetId((btid <= 0) ?
scene.getDefaultBaseTileSet() : (btid >> 16));
// determine if it fringes on our tile
var pri :int = _fringeConf.fringesOn(baseset, underset);
if (pri == -1) {
continue;
}
var fringer :FringerRec = FringerRec(_fringers.get(baseset));
if (fringer == null) {
fringer = new FringerRec(baseset, pri);
_fringers.put(baseset, fringer);
}
// now turn on the appropriate fringebits
fringer.bits |= FLAGMATRIX[y - row + 1][x - col + 1];
// See if a tile that fringes on us kills our passability,
// but don't count the default base tile against us, as
// we allow users to splash in the water.
if (passable && (btid > 0)) {
try {
var bt :BaseTile = BaseTile(_tMgr.getTile(btid));
passable = bt.isPassable();
} catch (nstse :NoSuchTileSetError) {
log.warning("Autofringer couldn't find a base set while attempting to " +
"figure passability", nstse);
}
}
}
}
// if nothing fringed, we're done
var numfringers :int = _fringers.size();
if (numfringers == 0) {
return null;
}
// otherwise compose a FringeTile from the specified fringes
var frecs :Array = _fringers.values();
return composeFringeTile(frecs, fringes, TileUtil.getTileHash(col, row), passable, masks);
}
/**
* Compose a FringeTile out of the various fringe images needed.
*/
protected function composeFringeTile (fringers :Array,
fringes :Map, hashValue :int, passable :Boolean, masks :Map) :FringeTile
{
// sort the array so that higher priority fringers get drawn first
Arrays.sort(fringers);
// Generate an identifier for the fringe tile being created as an array of the keys of its
// component tiles in the order they'll be drawn in the fringe tile.
var fringeIds :Array = [];
for each (var fringer :FringerRec in fringers) {
var indexes :Array = getFringeIndexes(fringer.bits);
var tsr :FringeTileSetRecord = _fringeConf.getFringe(fringer.baseset, hashValue);
var fringeset :int = tsr.fringe_tsid;
for each (var index :int in indexes) {
// Add a key for this tile as an int containing its base tile, the fringe set it's
// working with and the index used in that set.
fringeIds.push((fringer.baseset << 20) + (fringeset << 8) + index);
}
}
var frTile :FringeTile = new FringeTile(fringeIds, passable);
// If the fringes map contains something with the same fringe identifier, this will pull
// it out and we can use it instead.
var result :WeakReference = fringes.get(frTile);
if (result != null) {
var fringe :FringeTile = result.get();
if (fringe != null) {
return fringe;
}
}
// There's no fringe with the same identifier, so we need to create the tile.
var img :Bitmap = null;
getTileImageHelper1(img, hashValue, masks, fringers, 0, function (result :Bitmap) :void {
frTile.setImage(result);
});
fringes.put(frTile, new WeakReference(frTile));
return frTile;
}
protected function getTileImageHelper1 (img :Bitmap, hashValue :int, masks :Map,
fringers :Array, fringerIdx :int, callback :Function) :void
{
var fringer :FringerRec = fringers[fringerIdx];
var indexes :Array = getFringeIndexes(fringer.bits);
var tsr :FringeTileSetRecord = _fringeConf.getFringe(fringer.baseset, hashValue);
getTileImageHelper0(img, tsr, fringer.baseset, hashValue, masks, indexes, 0,
function (result :Bitmap) :void {
if (fringerIdx == fringers.length - 1) {
callback(result);
} else {
getTileImageHelper1(result, hashValue, masks, fringers, fringerIdx + 1,
callback);
}
});
}
protected function getTileImageHelper0 (img :Bitmap, tsr :FringeTileSetRecord,
baseset :int, hashValue :int, masks :Map, indexes :Array,
indexIdx :int, callback :Function) :void
{
try {
getTileImage(img, tsr, baseset, indexes[indexIdx], hashValue, masks,
function (result :Bitmap) :void {
if (indexIdx == indexes.length - 1) {
callback(result);
} else {
getTileImageHelper0(result, tsr, baseset, hashValue, masks, indexes,
indexIdx + 1, callback);
}
});
} catch (nstse :NoSuchTileSetError) {
log.warning("Autofringer couldn't find a needed tileset", nstse);
callback(null);
}
}
/**
* Retrieve or compose an image for the specified fringe.
*/
protected function getTileImage (img :Bitmap, tsr :FringeTileSetRecord ,
baseset :int, index :int, hashValue :int, masks :Map, callback :Function) :void
{
var fringeset :int = tsr.fringe_tsid;
var fset :TileSet = _tMgr.getTileSet(fringeset);
if (!tsr.mask) {
// oh good, this is easy
var stamp :Tile = fset.getTile(index);
if (stamp.getImage() != null) {
callback(stampTileImage(stamp, img, stamp.getWidth(), stamp.getHeight()));
} else {
stamp.notifyOnLoad(function(tile :Tile) :void {
callback(stampTileImage(stamp, img, stamp.getWidth(), stamp.getHeight()));
});
}
return;
}
// otherwise, it's a mask..
var maskkey :int = (baseset << 20) + (fringeset << 8) + index;
var mask :Bitmap = masks.get(maskkey);
if (mask == null) {
_tMgr.getTileSet(fringeset).getTileImage(index, null, function (fsrc :Bitmap) :void {
_tMgr.getTileSet(baseset).getTileImage(0, null, function (bsrc :Bitmap) :void {
mask = composeMaskedImage(fsrc, bsrc);
masks.put(maskkey, mask);
callback(stampTileImage(mask, img, mask.width, mask.height));
});
});
} else {
callback(stampTileImage(mask, img, mask.width, mask.height));
}
}
/**
* Create an image using the alpha channel from the first and the RGB values from the second.
*/
public static function composeMaskedImage (mask :Bitmap, base :Bitmap) :Bitmap
{
var data :BitmapData = new BitmapData(base.width, base.height);
data.copyPixels(base.bitmapData, new Rectangle(0, 0, base.width, base.height),
new Point(0, 0), mask.bitmapData, new Point(0, 0), false);
return new Bitmap(data);
}
/** Helper function for {@link #getTileImage}. */
protected function stampTileImage (stamp :Object, ftimg :Bitmap, width :int,
height :int) :Bitmap
{
// create the target image if necessary
if (ftimg == null) {
ftimg = new Bitmap(new BitmapData(width, height, true, 0x00000000));
}
var img :Bitmap;
if (stamp is Tile) {
img = Bitmap((Tile(stamp)).getImage());
} else {
img = Bitmap(stamp);
}
ftimg.bitmapData.draw(img);
return ftimg;
}
/**
* Get the fringe index specified by the fringebits. If no index is available, try breaking
* down the bits into contiguous regions of bits and look for indexes for those.
*/
protected function getFringeIndexes (bits :int) :Array
{
var index :int = BITS_TO_INDEX[bits];
if (index != -1) {
return [index];
}
// otherwise, split the bits into contiguous components
// look for a zero and start our first split
var start :int = 0;
while ((((1 << start) & bits) != 0) && (start < NUM_FRINGEBITS)) {
start++;
}
if (start == NUM_FRINGEBITS) {
// we never found an empty fringebit, and since index (above)
// was already -1, we have no fringe tile for these bits.. sad.
return new Array(0);
}
var indexes :Array = [];
var weebits :int = 0;
for (var ii :int = (start + 1) % NUM_FRINGEBITS; ii != start;
ii = (ii + 1) % NUM_FRINGEBITS) {
if (((1 << ii) & bits) != 0) {
weebits |= (1 << ii);
} else if (weebits != 0) {
index = BITS_TO_INDEX[weebits];
if (index != -1) {
indexes.push(index);
}
weebits = 0;
}
}
if (weebits != 0) {
index = BITS_TO_INDEX[weebits];
if (index != -1) {
indexes.push(index);
}
}
return indexes;
}
/**
* Allow subclasses to apply arbitrary modifications to tileset ids for whatever nefarious
* purposes they may have.
*/
protected function adjustTileSetId (tileSetId :int) :int
{
// by default, nothing.
return tileSetId;
}
// fringe bits
// see docs/miso/fringebits.png
//
protected static const NORTH :int = 1 << 0;
protected static const NORTHEAST :int = 1 << 1;
protected static const EAST :int = 1 << 2;
protected static const SOUTHEAST :int = 1 << 3;
protected static const SOUTH :int = 1 << 4;
protected static const SOUTHWEST :int = 1 << 5;
protected static const WEST :int = 1 << 6;
protected static const NORTHWEST :int = 1 << 7;
protected static const NUM_FRINGEBITS :int = 8;
// A matrix mapping adjacent tiles to which fringe bits they affect.
// (x and y are offset by +1, since we can't have -1 as an array index)
// again, see docs/miso/fringebits.png
//
protected static const FLAGMATRIX :Array= [
[ NORTHEAST, (NORTHEAST | EAST | SOUTHEAST), SOUTHEAST ],
[ (NORTHWEST | NORTH | NORTHEAST), 0, (SOUTHEAST | SOUTH | SOUTHWEST) ],
[ NORTHWEST, (NORTHWEST | WEST | SOUTHWEST), SOUTHWEST ]
];
/**
* The fringe tiles we use. These are the 17 possible tiles made up of continuous fringebits
* sections. Huh? see docs/miso/fringebits.png
*/
protected static const FRINGETILES :Array = [
SOUTHEAST,
SOUTHWEST | SOUTH | SOUTHEAST,
SOUTHWEST,
NORTHEAST | EAST | SOUTHEAST,
NORTHWEST | WEST | SOUTHWEST,
NORTHEAST,
NORTHWEST | NORTH | NORTHEAST,
NORTHWEST,
SOUTHWEST | WEST | NORTHWEST | NORTH | NORTHEAST,
NORTHWEST | NORTH | NORTHEAST | EAST | SOUTHEAST,
NORTHWEST | WEST | SOUTHWEST | SOUTH | SOUTHEAST,
SOUTHWEST | SOUTH | SOUTHEAST | EAST | NORTHEAST,
NORTHEAST | NORTH | NORTHWEST | WEST | SOUTHWEST | SOUTH | SOUTHEAST,
SOUTHEAST | EAST | NORTHEAST | NORTH | NORTHWEST | WEST | SOUTHWEST,
SOUTHWEST | SOUTH | SOUTHEAST | EAST | NORTHEAST | NORTH | NORTHWEST,
NORTHWEST | WEST | SOUTHWEST | SOUTH | SOUTHEAST | EAST | NORTHEAST,
// all the directions!
NORTH | NORTHEAST | EAST | SOUTHEAST | SOUTH | SOUTHWEST | WEST | NORTHWEST
];
// A reverse map of the above array, for quickly looking up which tile
// we want.
protected const INIT_CT :int = (1 << NUM_FRINGEBITS);
protected const BITS_TO_INDEX :Array = new Array(INIT_CT);
protected var _tMgr :TileManager;
protected var _fringeConf :FringeConfiguration;
protected var _fringers :Map = Maps.newMapOf(int);
}
}
import com.threerings.util.Comparable;
/**
* A record for holding information about a particular fringe as we're computing what it will
* look like.
*/
class FringerRec
implements Comparable
{
public var baseset :int;
public var priority :int;
public var bits :int;
public function FringerRec (base :int, pri :int)
{
baseset = base;
priority = pri;
}
public function compareTo (o :Object) :int
{
return priority - FringerRec(o).priority;
}
public function toString () :String
{
return "[base=" + baseset + ", pri=" + priority + ", bits="
+ bits.toString(16) + "]";
}
}
@@ -0,0 +1,113 @@
//
// $Id$
//
// Nenya library - tools for developing networked games
// Copyright (C) 2002-2012 Three Rings Design, Inc., All Rights Reserved
// http://code.google.com/p/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.miso.tile {
import com.threerings.util.Map;
import com.threerings.util.Maps;
import com.threerings.media.tile.TileSetIdMap;
/**
* Used to manage data about which base tilesets fringe on which others
* and how they fringe.
*/
public class FringeConfiguration
{
public static function fromXml (xml :XML, idMap :TileSetIdMap) :FringeConfiguration
{
var config :FringeConfiguration = new FringeConfiguration();
for each (var baseXml :XML in xml.base) {
config.addFringeRecord(FringeRecord.fromXml(baseXml, idMap));
}
return config;
}
/**
* Adds a parsed FringeRecord to this instance. This is used when parsing
* the fringerecords from xml.
*/
public function addFringeRecord (frec :FringeRecord) :void
{
_frecs.put(frec.base_tsid, frec);
}
/**
* If the first base tileset fringes upon the second, return the
* fringe priority of the first base tileset, otherwise return -1.
*/
public function fringesOn (first :int, second :int) :int
{
// Short-circuit if we're fringing on ourselves.
if (first == second) {
return -1;
}
var f1 :FringeRecord = _frecs.get(first);
// we better have a fringe record for the first
if (null != f1) {
// it had better have some tilesets defined
if (f1.tilesets.length > 0) {
var f2 :FringeRecord = _frecs.get(second);
// and we only fringe if second doesn't exist or has a lower
// priority
if ((null == f2) || (f1.priority > f2.priority)) {
return f1.priority;
}
}
}
return -1;
}
/**
* Get a random FringeTileSetRecord from amongst the ones
* listed for the specified base tileset.
*/
public function getFringe (baseset :int, hashValue :int) :FringeTileSetRecord
{
var f :FringeRecord = _frecs.get(baseset);
return f.tilesets[hashValue % f.tilesets.length];
}
/**
* Returns all the tilesets used to fringe with this baseset.
*/
public function getFringeSets (baseset :int) :Array
{
var f :FringeRecord = _frecs.get(baseset);
if (f == null) {
return [];
} else {
return f.tilesets.map(
function (element :FringeTileSetRecord, index:int, arr:Array) :int {
return element.fringe_tsid;
});
}
}
/** The mapping from base tileset id to fringerecord. */
protected var _frecs :Map = Maps.newMapOf(int);
}
}
@@ -0,0 +1,67 @@
//
// $Id$
//
// Nenya library - tools for developing networked games
// Copyright (C) 2002-2012 Three Rings Design, Inc., All Rights Reserved
// http://code.google.com/p/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.miso.tile {
import com.threerings.media.tile.TileSetIdMap;
import com.threerings.util.StringUtil;
public class FringeRecord
{
/** The tileset id of the base tileset to which this applies. */
public var base_tsid :int;
/** The fringe priority of this base tileset. */
public var priority :int;
/** A list of the possible tilesets that can be used for fringing. */
public var tilesets :Array = [];
public static function fromXml (xml :XML, idMap :TileSetIdMap) :FringeRecord
{
var rec :FringeRecord = new FringeRecord();
rec.base_tsid = idMap.getTileSetId(xml.@name);
rec.priority = xml.@priority;
for each (var tsXml :XML in xml.tileset) {
rec.addTileset(FringeTileSetRecord.fromXml(tsXml, idMap));
}
return rec;
}
/** Used when parsing the tilesets definitions. */
public function addTileset (record :FringeTileSetRecord) :void
{
tilesets.push(record);
}
/** Did everything parse well? */
public function isValid () :Boolean
{
return ((base_tsid != 0) && (priority > 0));
}
public function toString () :String
{
return "[base_tsid=" + base_tsid + ", priority=" + priority +
", tilesets=" + StringUtil.toString(tilesets) + "]";
}
}
}
@@ -0,0 +1,62 @@
//
// $Id$
//
// Nenya library - tools for developing networked games
// Copyright (C) 2002-2012 Three Rings Design, Inc., All Rights Reserved
// http://code.google.com/p/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.miso.tile {
import com.threerings.util.Arrays;
import com.threerings.util.Hashable;
import com.threerings.media.tile.BaseTile;
public class FringeTile extends BaseTile
implements Hashable
{
public function FringeTile (fringeIds :Array, passable :Boolean)
{
setPassable(passable);
_fringeIds = fringeIds;
}
public function equals (obj :Object) :Boolean
{
if (!(obj is FringeTile)) {
return false;
}
var fObj :FringeTile = FringeTile(obj);
return _passable == fObj._passable && Arrays.equals(_fringeIds, fObj._fringeIds);
}
public function hashCode () :int
{
var result :int = 33; // can't use Arrays.hashCode(long) as it's 1.5 only
for each (var key :int in _fringeIds) {
result = result * 37 + key;
}
if (_passable) {
result++;
}
return result;
}
/** The fringe keys of the tiles that went into this tile in the order they were drawn. */
protected var _fringeIds :Array;
}
}
@@ -0,0 +1,55 @@
//
// $Id$
//
// Nenya library - tools for developing networked games
// Copyright (C) 2002-2012 Three Rings Design, Inc., All Rights Reserved
// http://code.google.com/p/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.miso.tile {
import com.threerings.media.tile.TileSetIdMap;
import com.threerings.util.XmlUtil;
public class FringeTileSetRecord
{
/** The tileset id of the fringe tileset. */
public var fringe_tsid :int;
/** Is this a mask? */
public var mask :Boolean;
public static function fromXml (xml :XML, idMap :TileSetIdMap) :FringeTileSetRecord
{
var rec :FringeTileSetRecord = new FringeTileSetRecord();
rec.fringe_tsid = idMap.getTileSetId(xml.@name);
rec.mask = XmlUtil.getBooleanAttr(xml, "mask", false);
return rec;
}
/** Did everything parse well? */
public function isValid () :Boolean
{
return (fringe_tsid != 0);
}
public function toString () :String
{
return "[fringe_tsid=" + fringe_tsid + ", mask=" + mask + "]";
}
}
}
@@ -0,0 +1,57 @@
//
// $Id$
//
// Nenya library - tools for developing networked games
// Copyright (C) 2002-2012 Three Rings Design, Inc., All Rights Reserved
// http://code.google.com/p/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.miso.tile {
import com.threerings.media.tile.TileManager;
import com.threerings.util.Set;
/**
* Extends the basic tile manager and provides support for automatically generating fringes in
* between different types of base tiles in a scene.
*/
public class MisoTileManager extends TileManager
{
public function setFringer (fringer :AutoFringer) :void
{
_fringer = fringer;
}
public function getFringer () :AutoFringer
{
return _fringer;
}
override public function ensureLoaded (tileSets :Set, completeCallback :Function,
progressCallback :Function) :void
{
// Get those we'll use to fringe with us, too.
var fringeSets :Set = _fringer.getFringeSets(tileSets);
fringeSets.forEach(function (tsetId :int) :void {
tileSets.add(tsetId);
});
super.ensureLoaded(tileSets, completeCallback, progressCallback);
}
protected var _fringer :AutoFringer;
}
}

Some files were not shown because too many files have changed in this diff Show More