diff --git a/build.xml b/build.xml
index 49c8bb997..deebc208c 100644
--- a/build.xml
+++ b/build.xml
@@ -192,6 +192,9 @@
+
+
+
diff --git a/src/java/com/threerings/stage/Log.java b/src/java/com/threerings/stage/Log.java
new file mode 100644
index 000000000..81a155a03
--- /dev/null
+++ b/src/java/com/threerings/stage/Log.java
@@ -0,0 +1,38 @@
+//
+// $Id: Log.java 49 2001-08-09 00:32:53Z mdb $
+
+package com.threerings.stage;
+
+/**
+ * A placeholder class that contains a reference to the log object used by
+ * this package.
+ */
+public class Log
+{
+ public static com.samskivert.util.Log log =
+ new com.samskivert.util.Log("stage");
+
+ /** Convenience function. */
+ public static void debug (String message)
+ {
+ log.debug(message);
+ }
+
+ /** Convenience function. */
+ public static void info (String message)
+ {
+ log.info(message);
+ }
+
+ /** Convenience function. */
+ public static void warning (String message)
+ {
+ log.warning(message);
+ }
+
+ /** Convenience function. */
+ public static void logStackTrace (Throwable t)
+ {
+ log.logStackTrace(com.samskivert.util.Log.WARNING, t);
+ }
+}
diff --git a/src/java/com/threerings/stage/client/SceneColorizer.java b/src/java/com/threerings/stage/client/SceneColorizer.java
new file mode 100644
index 000000000..c52233867
--- /dev/null
+++ b/src/java/com/threerings/stage/client/SceneColorizer.java
@@ -0,0 +1,130 @@
+//
+// $Id: SceneColorizer.java 17027 2004-09-10 00:10:46Z ray $
+
+package com.threerings.stage.client;
+
+import java.util.HashMap;
+import java.util.Iterator;
+
+import com.threerings.media.image.ColorPository;
+import com.threerings.media.image.Colorization;
+import com.threerings.media.tile.TileSet;
+
+import com.threerings.miso.data.ObjectInfo;
+
+import com.threerings.stage.Log;
+import com.threerings.stage.data.StageScene;
+
+/**
+ * Handles colorization of object tiles in a scene.
+ */
+public class SceneColorizer implements TileSet.Colorizer
+{
+ /**
+ * Creates a scene colorizer for the supplied scene.
+ */
+ public SceneColorizer (ColorPository cpos, StageScene scene)
+ {
+ _cpos = cpos;
+ _scene = scene;
+
+ // enumerate the color ids for all possible colorization classes
+ for (Iterator iter = _cpos.enumerateClasses(); iter.hasNext(); ) {
+ String cname = ((ColorPository.ClassRecord)iter.next()).name;
+ _cids.put(cname, _cpos.enumerateColorIds(cname));
+ }
+ }
+
+ /**
+ * Set an auxiliary colorizer that overrides our colorizations.
+ */
+ public void setAuxiliary (TileSet.Colorizer aux)
+ {
+ _aux = aux;
+ }
+
+ /**
+ * Obtains a colorizer for the supplied scene object.
+ */
+ public TileSet.Colorizer getColorizer (final ObjectInfo oinfo)
+ {
+ // if the object has no custom colorizations, return the default
+ // colorizer
+ if (oinfo.zations == 0) {
+ return this;
+ }
+
+ // otherwise create a custom colorizer that returns this object's
+ // custom colorization assignments
+ return new TileSet.Colorizer() {
+ public Colorization getColorization (int index, String zation) {
+ int colorId = 0;
+ switch (index) {
+ case 0: colorId = oinfo.getPrimaryZation(); break;
+ case 1: colorId = oinfo.getSecondaryZation(); break;
+ }
+ if (colorId == 0) {
+ return SceneColorizer.this.getColorization(index, zation);
+ } else {
+ return _cpos.getColorization(zation, colorId);
+ }
+ }
+ };
+ }
+
+ // documentation inherited from interface TileSet.Colorizer
+ public Colorization getColorization (int index, String zation)
+ {
+ // This method is called when an object in the scene has no colorization
+ // of its own defined for a particular color class.
+ if (_aux != null) {
+ Colorization c = _aux.getColorization(index, zation);
+ if (c != null) {
+ return c;
+ }
+ }
+ return _cpos.getColorization(zation, getColorId(zation));
+ }
+
+ /**
+ * Get the colorId to use for the specified colorization.
+ */
+ public int getColorId (String zation)
+ {
+ // 1. We see if the scene contains a default color we should use.
+ ColorPository.ClassRecord rec = _cpos.getClassRecord(zation);
+ int colorId = _scene.getDefaultColor(rec.classId);
+ if (colorId == -1) {
+ // 2. If the scene does not contain a color, see if a default
+ // is defined for that color class.
+ ColorPository.ColorRecord def = rec.getDefault();
+ if (def != null) {
+ return def.colorId;
+ }
+
+ // 3. If there are no defaults whatsoever, just hash on the sceneId.
+ int[] cids = (int[])_cids.get(zation);
+ if (cids == null) {
+ Log.warning("Zoiks, have no colorizations for '" +
+ zation + "'.");
+ return -1;
+ } else {
+ colorId = cids[_scene.getZoneId() % cids.length];
+ }
+ }
+ return colorId;
+ }
+
+ /** An auxiliary colorizer which may temporarily return
+ * non-standard colorizations. */
+ protected TileSet.Colorizer _aux;
+
+ /** The entity from which we obtain colorization info. */
+ protected ColorPository _cpos;
+
+ /** The scene for which we're providing zations. */
+ protected StageScene _scene;
+
+ /** Contains our colorization class information. */
+ protected HashMap _cids = new HashMap();
+}
diff --git a/src/java/com/threerings/stage/client/StageSceneController.java b/src/java/com/threerings/stage/client/StageSceneController.java
new file mode 100644
index 000000000..06e459507
--- /dev/null
+++ b/src/java/com/threerings/stage/client/StageSceneController.java
@@ -0,0 +1,33 @@
+//
+// $Id: WorldSceneController.java 9625 2003-06-11 04:17:18Z mdb $
+
+package com.threerings.stage.client;
+
+import com.threerings.crowd.client.PlaceView;
+
+import com.threerings.whirled.data.SceneUpdate;
+import com.threerings.whirled.spot.client.SpotSceneController;
+
+import com.threerings.stage.util.StageContext;
+
+/**
+ * Extends the {@link SpotSceneController} with functionality specific to
+ * displaying Stage scenes.
+ */
+public class StageSceneController extends SpotSceneController
+{
+ // documentation inherited
+ protected PlaceView createPlaceView ()
+ {
+ return new StageScenePanel((StageContext)_ctx, this);
+ }
+
+ // documentation inherited
+ protected void sceneUpdated (SceneUpdate update)
+ {
+ super.sceneUpdated(update);
+
+ // let the scene panel know to rethink everything
+ ((StageScenePanel)_view).sceneUpdated(update);
+ }
+}
diff --git a/src/java/com/threerings/stage/client/StageScenePanel.java b/src/java/com/threerings/stage/client/StageScenePanel.java
new file mode 100644
index 000000000..62760469b
--- /dev/null
+++ b/src/java/com/threerings/stage/client/StageScenePanel.java
@@ -0,0 +1,659 @@
+//
+// $Id: WorldScenePanel.java 18366 2004-12-15 22:56:58Z ray $
+
+package com.threerings.stage.client;
+
+import java.awt.AlphaComposite;
+import java.awt.BasicStroke;
+import java.awt.Color;
+import java.awt.Composite;
+import java.awt.Dimension;
+import java.awt.Graphics2D;
+import java.awt.Point;
+import java.awt.Polygon;
+import java.awt.Rectangle;
+import java.awt.Shape;
+import java.awt.Stroke;
+import java.awt.geom.Ellipse2D;
+
+import java.awt.event.KeyEvent;
+import java.awt.event.KeyListener;
+import java.awt.event.MouseEvent;
+
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.Iterator;
+
+import com.samskivert.swing.Controller;
+import com.samskivert.swing.ControllerProvider;
+import com.samskivert.swing.util.SwingUtil;
+import com.samskivert.util.RuntimeAdjust;
+
+import com.samskivert.util.Tuple;
+import com.threerings.util.StreamableArrayList;
+
+import com.threerings.media.tile.ObjectTile;
+import com.threerings.media.tile.TileSet;
+import com.threerings.media.tile.UniformTileSet;
+
+import com.threerings.miso.client.MisoScenePanel;
+import com.threerings.miso.client.SceneObject;
+import com.threerings.miso.client.SceneObjectTip;
+import com.threerings.miso.data.ObjectInfo;
+import com.threerings.miso.util.MisoUtil;
+
+import com.threerings.crowd.client.PlaceView;
+import com.threerings.crowd.data.PlaceObject;
+
+import com.threerings.whirled.data.SceneUpdate;
+import com.threerings.whirled.spot.data.Cluster;
+import com.threerings.whirled.spot.data.Location;
+import com.threerings.whirled.spot.data.Portal;
+
+import com.threerings.stage.Log;
+import com.threerings.stage.data.StageMisoSceneModel;
+import com.threerings.stage.data.StageScene;
+import com.threerings.stage.data.StageSceneModel;
+import com.threerings.stage.util.StageContext;
+import com.threerings.stage.util.StageSceneUtil;
+
+/**
+ * Extends the basic Miso scene panel with Stage fun stuff like portals,
+ * clusters and locations.
+ */
+public class StageScenePanel extends MisoScenePanel
+ implements ControllerProvider, KeyListener, PlaceView
+{
+ /** An action command generated when the user clicks on a location
+ * within the scene. */
+ public static final String LOCATION_CLICKED = "LocationClicked";
+
+ /** An action command generated when something besides the user wants
+ * us to move to a location. */
+ public static final String LOCATION_REQUESTED = "LocationRequested";
+
+ /** An action command generated when a cluster is clicked. */
+ public static final String CLUSTER_CLICKED = "ClusterClicked";
+
+ /** Show flag that indicates we should show all clusters. */
+ public static final int SHOW_CLUSTERS = (1 << 1);
+
+ /** Show flag that indicates we should render known land plots
+ * (expensive, don't turn this on willy nilly). */
+ public static final int SHOW_PLOTS = (1 << 2);
+
+ /**
+ * Constructs a stage scene view panel.
+ */
+ public StageScenePanel (StageContext ctx, StageSceneController ctrl)
+ {
+ super(ctx, StageSceneUtil.getMetrics());
+
+ // keep these around for later
+ _ctx = ctx;
+ _ctrl = ctrl;
+ _ctrl.setControlledPanel(this);
+
+ // no layout manager
+ setLayout(null);
+ }
+
+ /**
+ * Get the tileset colorizer in use in this scene.
+ */
+ public SceneColorizer getColorizer ()
+ {
+ return _rizer;
+ }
+
+ // documentation inherited
+ protected TileSet.Colorizer getColorizer (ObjectInfo oinfo)
+ {
+ return _rizer.getColorizer(oinfo);
+ }
+
+ /**
+ * Returns the scene being displayed by this panel. Do not modify it.
+ */
+ public StageScene getScene ()
+ {
+ return _scene;
+ }
+
+ /**
+ * Sets the scene managed by the panel.
+ */
+ public void setScene (StageScene scene)
+ {
+ _scene = scene;
+ if (_scene != null) {
+ recomputePortals();
+ setSceneModel(StageMisoSceneModel.getSceneModel(
+ scene.getSceneModel()));
+ _rizer = new SceneColorizer(_ctx.getColorPository(), scene);
+ } else {
+ Log.warning("Zoiks! We can't display a null scene!");
+ // TODO: display something to the user letting them know that
+ // we're so hosed that we don't even know what time it is
+ }
+ }
+
+ /**
+ * Called when we have received a scene update from the server.
+ */
+ public void sceneUpdated (SceneUpdate update)
+ {
+ // recompute the portals as those may well have changed
+ recomputePortals();
+
+ // we go aheand and completely replace our scene model which will
+ // reload the whole good goddamned business; it is a little
+ // shocking to the user, but it's guaranteed to work
+ refreshScene();
+ }
+
+ /**
+ * Computes a set of display objects for the portals in this scene.
+ */
+ protected void recomputePortals ()
+ {
+ // create scene objects for our portals
+ UniformTileSet ots = loadPortalTileSet();
+
+ _portobjs.clear();
+ for (Iterator iter = _scene.getPortals(); iter.hasNext(); ) {
+ Portal portal = (Portal) iter.next();
+ Point p = getScreenCoords(portal.x, portal.y);
+ int tx = MisoUtil.fullToTile(portal.x);
+ int ty = MisoUtil.fullToTile(portal.y);
+ Point ts = MisoUtil.tileToScreen(_metrics, tx, ty, new Point());
+
+// Log.info("Added portal " + portal +
+// " [screen=" + StringUtil.toString(p) +
+// ", tile=" + StringUtil.coordsToString(tx, ty) +
+// ", tscreen=" + StringUtil.toString(ts) + "].");
+
+ ObjectInfo info = new ObjectInfo(0, tx, ty);
+ info.action = "portal:" + portal.portalId;
+
+ // TODO: cache me
+ ObjectTile tile = new PortalObjectTile(
+ ts.x + _metrics.tilehwid - p.x + (PORTAL_ICON_WIDTH / 2),
+ ts.y + _metrics.tilehei - p.y + (PORTAL_ICON_HEIGHT / 2));
+ tile.setImage(ots.getTileMirage(portal.orient));
+
+ _portobjs.add(new SceneObject(this, info, tile) {
+ public boolean setHovered (boolean hovered) {
+ ((PortalObjectTile)this.tile).hovered = hovered;
+ return isResponsive();
+ }
+ });
+ }
+ }
+
+ // documentation inherited
+ protected void recomputeVisible ()
+ {
+ super.recomputeVisible();
+
+ // add our visible portal objects to the list of visible objects
+ for (int ii = 0, ll = _portobjs.size(); ii < ll; ii++) {
+ SceneObject pobj = (SceneObject)_portobjs.get(ii);
+ if (pobj.bounds != null && _vbounds.intersects(pobj.bounds)) {
+ _vizobjs.add(pobj);
+ }
+ }
+ }
+
+ // documentation inherited from interface ControllerProvider
+ public Controller getController ()
+ {
+ return _ctrl;
+ }
+
+ // documentation inherited from interface KeyListener
+ public void keyPressed (KeyEvent e)
+ {
+ if (e.getKeyCode() == KeyEvent.VK_ALT) {
+ // display all tooltips
+ setShowFlags(SHOW_TIPS, true);
+ }
+ }
+
+ // documentation inherited from interface KeyListener
+ public void keyReleased (KeyEvent e)
+ {
+ if (e.getKeyCode() == KeyEvent.VK_ALT) {
+ // stop displaying all tooltips
+ setShowFlags(SHOW_TIPS, defaultShowTips());
+ }
+ }
+
+ // documentation inherited from interface PlaceView
+ public void willEnterPlace (PlaceObject plobj)
+ {
+ }
+
+ // documentation inherited from interface PlaceView
+ public void didLeavePlace (PlaceObject plobj)
+ {
+ }
+
+ /**
+ * Returns true if we should always show the object tooltips by
+ * default, false if they should only be shown while the 'Alt' key is
+ * depressed.
+ */
+ protected boolean defaultShowTips ()
+ {
+ return false;
+ }
+
+ // documentation inherited
+ public void keyTyped (KeyEvent e)
+ {
+ // nothing
+ }
+
+ // documentation inherited
+ protected boolean handleMousePressed (Object hobject, MouseEvent event)
+ {
+ // let our parent have a crack at the old mouse press
+ if (super.handleMousePressed(hobject, event)) {
+ return true;
+ }
+
+ // if the hover object is a cluster, we clicked it!
+ if (event.getButton() == MouseEvent.BUTTON1) {
+ if (hobject instanceof Cluster) {
+ int mx = event.getX(), my = event.getY();
+ Object actarg = new Tuple(hobject, new Point(mx, my));
+ Controller.postAction(this, CLUSTER_CLICKED, actarg);
+ } else {
+ // post an action indicating that we've clicked on a location
+ Point lc = MisoUtil.screenToFull(
+ _metrics, event.getX(), event.getY(), new Point());
+ Controller.postAction(this, LOCATION_CLICKED,
+ new Location(lc.x, lc.y, (byte)0));
+ }
+ return true;
+ }
+ return false;
+ }
+
+ /**
+ * Called when our show flags have changed.
+ */
+ protected void showFlagsDidChange (int oldflags)
+ {
+ super.showFlagsDidChange(oldflags);
+
+ if ((oldflags & SHOW_CLUSTERS) != (_showFlags & SHOW_CLUSTERS)) {
+ // dirty every cluster rectangle
+ Iterator iter = _clusters.values().iterator();
+ while (iter.hasNext()) {
+ dirtyCluster((Shape)iter.next());
+ }
+ }
+ }
+
+ /**
+ * Called when a real cluster is created or updated in the scene.
+ */
+ protected void clusterUpdated (Cluster cluster)
+ {
+ // compute a screen rectangle that contains all possible "spots"
+ // in this cluster
+ ArrayList spots = StageSceneUtil.getClusterLocs(cluster);
+ Rectangle cbounds = null;
+ for (int ii = 0, ll = spots.size(); ii < ll; ii++) {
+ Location loc = (Location)spots.get(ii);
+ Point sp = getScreenCoords(loc.x, loc.y);
+ if (cbounds == null) {
+ cbounds = new Rectangle(sp.x, sp.y, 0, 0);
+ } else {
+ cbounds.add(sp.x, sp.y);
+ }
+ }
+
+ if (cbounds == null) {
+ // if we found no one actually in this cluster, nix it
+ removeCluster(cluster.clusterOid);
+ } else {
+ // otherwise have the view update the cluster
+ updateCluster(cluster, cbounds);
+ }
+ }
+
+ /**
+ * Adds or updates the specified cluster in the view. Metrics will be
+ * created that allow the cluster to be rendered and hovered over
+ * (which would make it the active cluster as indicated by {@link
+ * #getActiveCluster}).
+ *
+ * @param cluster the cluster record to be added.
+ * @param bounds the screen coordinates that bound the occupants of
+ * the cluster.
+ */
+ public void updateCluster (Cluster cluster, Rectangle bounds)
+ {
+ // dirty any old bounds
+ dirtyCluster(cluster);
+
+ // compute the screen coordinate bounds of this cluster
+ Shape shape = new Ellipse2D.Float(
+ bounds.x, bounds.y, bounds.width, bounds.height);
+ _clusters.put(cluster, shape);
+
+ // if the mouse is inside these bounds, we highlight this cluster
+ Shape mshape = new Ellipse2D.Float(
+ bounds.x-CLUSTER_SLOP, bounds.y-CLUSTER_SLOP,
+ bounds.width+2*CLUSTER_SLOP, bounds.height+2*CLUSTER_SLOP);
+ _clusterWells.put(cluster, mshape);
+
+ // dirty our new bounds
+ dirtyCluster(shape);
+ }
+
+ /**
+ * Removes the specified cluster from the view.
+ *
+ * @return true if such a cluster existed and was removed.
+ */
+ public boolean removeCluster (int clusterOid)
+ {
+ Cluster key = new Cluster();
+ key.clusterOid = clusterOid;
+ _clusterWells.remove(key);
+ Shape shape = (Shape)_clusters.remove(key);
+ if (shape == null) {
+ return false;
+ }
+
+ dirtyCluster(shape);
+ // clear out the hover object if this cluster was it
+ if (_hobject instanceof Cluster &&
+ ((Cluster)_hobject).clusterOid == clusterOid) {
+ _hobject = null;
+ }
+ return true;
+ }
+
+ /**
+ * A place for subclasses to react to the hover object changing.
+ * One of the supplied arguments may be null.
+ */
+ protected void hoverObjectChanged (Object oldHover, Object newHover)
+ {
+ super.hoverObjectChanged(oldHover, newHover);
+
+ if (oldHover instanceof Cluster) {
+ dirtyCluster((Cluster)oldHover);
+ }
+ if (newHover instanceof Cluster) {
+ dirtyCluster((Cluster)newHover);
+ }
+ }
+
+ /**
+ * 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 Object computeOverHover (int mx, int my)
+ {
+ 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 Object computeUnderHover (int mx, int my)
+ {
+ if (!isResponsive()) {
+ return null;
+ }
+
+ // if the current hover object is a cluster, see if we're still in
+ // that cluster
+ if (_hobject instanceof Cluster) {
+ Cluster cluster = (Cluster)_hobject;
+ if (containsPoint(cluster, mx, my)) {
+ return cluster;
+ }
+ }
+
+ // otherwise, check to see if the mouse is in some new cluster
+ Iterator iter = _clusters.keySet().iterator();
+ while (iter.hasNext()) {
+ Cluster cclust = (Cluster)iter.next();
+ if (containsPoint(cclust, mx, my)) {
+ return cclust;
+ }
+ }
+
+ return null;
+ }
+
+ /**
+ * Returns true if the specified cluster contains the supplied screen
+ * coordinate.
+ */
+ protected boolean containsPoint (Cluster cluster, int mx, int my)
+ {
+ Shape shape = (Shape)_clusterWells.get(cluster);
+ return (shape == null) ? false : shape.contains(mx, my);
+ }
+
+ /**
+ * Dirties the supplied cluster.
+ */
+ protected void dirtyCluster (Cluster cluster)
+ {
+ if (cluster != null) {
+ dirtyCluster((Shape)_clusters.get(cluster));
+ }
+ }
+
+ /**
+ * Dirties the supplied cluster rectangle.
+ */
+ protected void dirtyCluster (Shape shape)
+ {
+ if (shape != null) {
+ Rectangle r = shape.getBounds();
+ _remgr.invalidateRegion(
+ r.x - (CLUSTER_PAD / 2),
+ r.y - (CLUSTER_PAD / 2),
+ r.width + (CLUSTER_PAD * 3 / 2),
+ r.height + (CLUSTER_PAD * 3 / 2));
+ }
+ }
+
+ /**
+ * Returns the portal at the specified full coordinates or null if no
+ * portal exists at said coordinates.
+ */
+ public Portal getPortal (int fullX, int fullY)
+ {
+ Iterator iter = _scene.getPortals();
+ while (iter.hasNext()) {
+ Portal portal = (Portal)iter.next();
+ if (portal.x == fullX && portal.y == fullY) {
+ return portal;
+ }
+ }
+ return null;
+ }
+
+ // documentation inherited
+ protected void paintBaseDecorations (Graphics2D gfx, Rectangle clip)
+ {
+ super.paintBaseDecorations(gfx, clip);
+
+ paintClusters(gfx, clip);
+ }
+
+ /**
+ * Paints any visible clusters.
+ */
+ protected void paintClusters (Graphics2D gfx, Rectangle clip)
+ {
+ // remember how daddy's things were arranged
+ Object oalias = SwingUtil.activateAntiAliasing(gfx);
+ Composite ocomp = gfx.getComposite();
+ Stroke ostroke = gfx.getStroke();
+
+ // get ready to draw clusters
+ gfx.setStroke(CLUSTER_STROKE);
+ gfx.setColor(CLUSTER_COLOR);
+
+ if (checkShowFlag(SHOW_CLUSTERS)
+ /* || // _alwaysShowClusters.getValue() */) {
+ // draw all clusters
+ Iterator iter = _clusters.keySet().iterator();
+ while (iter.hasNext()) {
+ drawCluster(gfx, clip, (Cluster)iter.next());
+ }
+
+ } else if (_hobject instanceof Cluster) {
+ // or just draw the active cluster
+ drawCluster(gfx, clip, (Cluster)_hobject);
+ }
+
+ // put back daddy's things
+ gfx.setComposite(ocomp);
+ gfx.setStroke(ostroke);
+ SwingUtil.restoreAntiAliasing(gfx, oalias);
+ }
+
+ /**
+ * Draw the cluster specified by the rectangle.
+ */
+ protected void drawCluster (Graphics2D gfx, Rectangle clip, Cluster cluster)
+ {
+ Shape shape = (Shape)_clusters.get(cluster);
+ if ((shape != null) && shape.intersects(clip)) {
+ if (_hobject == cluster) {
+ gfx.setComposite(HIGHLIGHT_ALPHA);
+ } else {
+ gfx.setComposite(SHOWN_ALPHA);
+ }
+ gfx.draw(shape);
+ }
+ }
+
+ /**
+ * Returns true if the specified location is associated with a portal.
+ */
+ protected boolean isPortal (Location loc)
+ {
+ Iterator iter = _scene.getPortals();
+ while (iter.hasNext()) {
+ Portal p = (Portal)iter.next();
+ if (p.x == loc.x && p.y == loc.y) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ /**
+ * Loads up the tileset used to render the portal arrows.
+ */
+ protected UniformTileSet loadPortalTileSet ()
+ {
+// return YoUI.client.loadTileSet(
+// "media/yohoho/icons/portal_arrows.png",
+// PORTAL_ICON_WIDTH, PORTAL_ICON_HEIGHT);
+ return null;
+ }
+
+ /** Used to render portals as objects in a scene. */
+ protected class PortalObjectTile extends ObjectTile
+ {
+ public boolean hovered = false;
+
+ public PortalObjectTile (int ox, int oy)
+ {
+ setOrigin(ox, oy);
+ }
+
+ public void paint (Graphics2D gfx, int x, int y)
+ {
+ Composite ocomp = gfx.getComposite();
+ if (!isResponsive() || !hovered) {
+ gfx.setComposite(INACTIVE_PORTAL_ALPHA);
+ }
+ super.paint(gfx, x, y);
+ gfx.setComposite(ocomp);
+ }
+ }
+
+ /** A reference to our client context. */
+ protected StageContext _ctx;
+
+ /** The controller with which we work in tandem. */
+ protected StageSceneController _ctrl;
+
+ /** Our currently displayed scene. */
+ protected StageScene _scene;
+
+ /** Contains scene objects for our portals. */
+ protected ArrayList _portobjs = new ArrayList();
+
+ /** Shapes describing the clusters, indexed by cluster. */
+ protected HashMap _clusters = new HashMap();
+
+ /** Shapes describing the clusters, indexed by cluster. */
+ protected HashMap _clusterWells = new HashMap();
+
+ /** Handles scene object colorization. */
+ protected SceneColorizer _rizer;
+
+// /** A debug hook that toggles always-on rendering of clusters. */
+// protected static RuntimeAdjust.BooleanAdjust _alwaysShowClusters =
+// new RuntimeAdjust.BooleanAdjust(
+// "Causes all clusters to always be rendered.",
+// "yohoho.miso.always_show_clusters",
+// ClientPrefs.config, false);
+
+ /** The width of the portal icons. */
+ protected static final int PORTAL_ICON_WIDTH = 48;
+
+ /** The height of the portal icons. */
+ protected static final int PORTAL_ICON_HEIGHT = 48;
+
+ /** The distance within which the mouse must be from a location
+ * in order to highlight it. */
+ protected static final int MAX_LOCATION_DIST = 25;
+
+ /** The amount the stroke a cluster. */
+ protected static final int CLUSTER_PAD = 4;
+
+ /** The width with which to draw the cluster. */
+ protected static final Stroke CLUSTER_STROKE = new BasicStroke(CLUSTER_PAD);
+
+ /** The color used to render clusters. */
+ protected static final Color CLUSTER_COLOR = Color.ORANGE;
+
+ /** Alpha level used to hightlight locations or clusters. */
+ protected static final Composite HIGHLIGHT_ALPHA =
+ AlphaComposite.getInstance(AlphaComposite.SRC_OVER, 0.3f);
+
+ /** Alpha level used to render clusters when they're not selected. */
+ protected static final Composite SHOWN_ALPHA =
+ AlphaComposite.getInstance(AlphaComposite.SRC_OVER, 0.15f);
+
+ /** The alpha with which to render inactive portals. */
+ protected static final Composite INACTIVE_PORTAL_ALPHA = HIGHLIGHT_ALPHA;
+
+ /** The number of pixels outside a cluster when we assume the mouse is
+ * "over" that cluster. */
+ protected static final int CLUSTER_SLOP = 25;
+}
diff --git a/src/java/com/threerings/stage/client/StageSceneService.java b/src/java/com/threerings/stage/client/StageSceneService.java
new file mode 100644
index 000000000..683ba4472
--- /dev/null
+++ b/src/java/com/threerings/stage/client/StageSceneService.java
@@ -0,0 +1,21 @@
+//
+// $Id: WorldSceneService.java 14426 2004-03-12 12:12:32Z mdb $
+
+package com.threerings.stage.client;
+
+import com.threerings.presents.client.Client;
+import com.threerings.presents.client.InvocationService;
+
+import com.threerings.miso.data.ObjectInfo;
+
+/**
+ * Provides services relating to Stage scenes.
+ */
+public interface StageSceneService extends InvocationService
+{
+ /**
+ * Requests to add the supplied object to the current scene.
+ */
+ public void addObject (Client client, ObjectInfo info,
+ ConfirmListener listener);
+}
diff --git a/src/java/com/threerings/stage/data/AddObjectUpdate.java b/src/java/com/threerings/stage/data/AddObjectUpdate.java
new file mode 100644
index 000000000..8e7178ac2
--- /dev/null
+++ b/src/java/com/threerings/stage/data/AddObjectUpdate.java
@@ -0,0 +1,53 @@
+//
+// $Id: AddObjectUpdate.java 15953 2004-06-11 23:40:47Z ray $
+
+package com.threerings.stage.data;
+
+import com.threerings.miso.data.ObjectInfo;
+
+import com.threerings.whirled.data.SceneModel;
+import com.threerings.whirled.data.SceneUpdate;
+
+/**
+ * A scene update that is broadcast when an object has been added to a
+ * scene.
+ */
+public class AddObjectUpdate extends SceneUpdate
+{
+ /** The info on the object. */
+ public ObjectInfo info;
+
+ /** If non-null, a list of objects to remove from the scene. */
+ public ObjectInfo[] casualties;
+
+ /**
+ * Initializes this update with all necessary data.
+ *
+ * @param casualties optional, a list of objects to remove.
+ */
+ public void init (int targetId, int targetVersion, ObjectInfo info,
+ ObjectInfo[] casualties)
+ {
+ init(targetId, targetVersion);
+ this.info = info;
+ this.casualties = casualties;
+ }
+
+ // documentation inherited
+ public void apply (SceneModel model)
+ {
+ super.apply(model);
+
+ StageMisoSceneModel mmodel = StageMisoSceneModel.getSceneModel(model);
+
+ // wipe out the objects that need to go
+ if (casualties != null) {
+ for (int ii = 0; ii < casualties.length; ii++) {
+ mmodel.removeObject(casualties[ii]);
+ }
+ }
+
+ // add the new object
+ mmodel.addObject(info);
+ }
+}
diff --git a/src/java/com/threerings/stage/data/DefaultColorUpdate.java b/src/java/com/threerings/stage/data/DefaultColorUpdate.java
new file mode 100644
index 000000000..81bd91b9c
--- /dev/null
+++ b/src/java/com/threerings/stage/data/DefaultColorUpdate.java
@@ -0,0 +1,39 @@
+//
+// $Id: DefaultColorUpdate.java 16551 2004-07-27 20:53:28Z ray $
+
+package com.threerings.stage.data;
+
+import com.threerings.whirled.data.SceneModel;
+import com.threerings.whirled.data.SceneUpdate;
+
+/**
+ * Update to change the default colorization for objects in a scene which
+ * do not define their own colorization.
+ */
+public class DefaultColorUpdate extends SceneUpdate
+{
+ /** The class id of the colorization we're changing. */
+ public int classId;
+
+ /** The color id to set as the new default, or -1 to remove the default. */
+ public int colorId;
+
+ /**
+ * Initializes this update.
+ */
+ public void init (int targetId, int targetVersion, int classId, int colorId)
+ {
+ init(targetId, targetVersion);
+ this.classId = classId;
+ this.colorId = colorId;
+ }
+
+ // documentation inherited
+ public void apply (SceneModel model)
+ {
+ super.apply(model);
+
+ StageSceneModel smodel = (StageSceneModel)model;
+ smodel.setDefaultColor(classId, colorId);
+ }
+}
diff --git a/src/java/com/threerings/stage/data/StageMisoSceneModel.java b/src/java/com/threerings/stage/data/StageMisoSceneModel.java
new file mode 100644
index 000000000..f6ea348e5
--- /dev/null
+++ b/src/java/com/threerings/stage/data/StageMisoSceneModel.java
@@ -0,0 +1,64 @@
+//
+// $Id: YoMisoSceneModel.java 9680 2003-06-13 02:16:00Z mdb $
+
+package com.threerings.stage.data;
+
+import com.threerings.miso.data.SparseMisoSceneModel;
+
+import com.threerings.whirled.data.AuxModel;
+import com.threerings.whirled.data.SceneModel;
+
+/**
+ * Extends the {@link SparseMisoSceneModel} with the necessary interface
+ * to wire it up to the Whirled auxiliary model system.
+ */
+public class StageMisoSceneModel extends SparseMisoSceneModel
+ implements AuxModel
+{
+ /** The width (in tiles) of a scene section. */
+ public static final int SECTION_WIDTH = 9;
+
+ /** The height (in tiles) of a scene section. */
+ public static final int SECTION_HEIGHT = 9;
+
+ /**
+ * Creates a completely uninitialized scene model.
+ */
+ public StageMisoSceneModel ()
+ {
+ super(SECTION_WIDTH, SECTION_HEIGHT);
+ }
+
+ /**
+ * Locates and returns the {@link StageMisoSceneModel} among the
+ * auxiliary scene models associated with the supplied scene model.
+ * null is returned if no miso scene model could be
+ * found.
+ */
+ public static StageMisoSceneModel getSceneModel (SceneModel model)
+ {
+ for (int ii = 0; ii < model.auxModels.length; ii++) {
+ if (model.auxModels[ii] instanceof StageMisoSceneModel) {
+ return (StageMisoSceneModel)model.auxModels[ii];
+ }
+ }
+ return null;
+ }
+
+ /**
+ * Returns the section key for the specified tile coordinate.
+ */
+ public int getSectionKey (int x, int y)
+ {
+ return key(x, y);
+ }
+
+ /**
+ * Returns the section identified by the specified key, or null if no
+ * section exists for that key.
+ */
+ public Section getSection (int key)
+ {
+ return (Section)_sections.get(key);
+ }
+}
diff --git a/src/java/com/threerings/stage/data/StageScene.java b/src/java/com/threerings/stage/data/StageScene.java
new file mode 100644
index 000000000..38a81f328
--- /dev/null
+++ b/src/java/com/threerings/stage/data/StageScene.java
@@ -0,0 +1,235 @@
+//
+// $Id: YoScene.java 17013 2004-09-08 02:39:09Z ray $
+
+package com.threerings.stage.data;
+
+import java.util.ArrayList;
+import java.util.Iterator;
+
+import com.threerings.miso.data.ObjectInfo;
+import com.threerings.miso.util.MisoUtil;
+
+import com.threerings.crowd.data.PlaceConfig;
+
+import com.threerings.whirled.data.Scene;
+import com.threerings.whirled.data.SceneImpl;
+import com.threerings.whirled.data.SceneUpdate;
+
+import com.threerings.whirled.spot.data.Portal;
+import com.threerings.whirled.spot.data.SpotScene;
+import com.threerings.whirled.spot.data.SpotSceneImpl;
+import com.threerings.whirled.spot.data.SpotSceneModel;
+
+import com.threerings.stage.Log;
+
+/**
+ * The implementation of the Stage scene interface.
+ */
+public class StageScene extends SceneImpl
+ implements Scene, SpotScene, Cloneable
+{
+ /**
+ * Creates an instance that will obtain data from the supplied scene
+ * model and place config.
+ */
+ public StageScene (StageSceneModel model, PlaceConfig config)
+ {
+ super(model, config);
+ _model = model;
+ _sdelegate = new SpotSceneImpl(SpotSceneModel.getSceneModel(_model));
+ readInterestingObjects();
+ }
+
+ protected void readInterestingObjects ()
+ {
+ _objects.clear();
+ StageMisoSceneModel mmodel = StageMisoSceneModel.getSceneModel(_model);
+ if (mmodel != null) {
+ mmodel.getInterestingObjects(_objects);
+ }
+ }
+
+ // documentation inherited
+ public void updateReceived (SceneUpdate update)
+ {
+ super.updateReceived(update);
+
+ // update our spot scene delegate
+ _sdelegate.updateReceived();
+
+ // re-read our interesting objects
+ readInterestingObjects();
+ }
+
+ /**
+ * Returns the scene type (e.g. "world", "port", "bank", etc.).
+ */
+ public String getType ()
+ {
+ return _model.type;
+ }
+
+ /**
+ * Returns the zone id to which this scene belongs.
+ */
+ public int getZoneId ()
+ {
+ return _model.zoneId;
+ }
+
+ /**
+ * Sets the type of this scene.
+ */
+ public void setType (String type)
+ {
+ _model.type = type;
+ }
+
+ /**
+ * Get the default color id to use for the specified colorization class,
+ * or -1 if no default is set.
+ */
+ public int getDefaultColor (int classId)
+ {
+ return _model.getDefaultColor(classId);
+ }
+
+ /**
+ * Set the default color to use for the specified colorization class id.
+ * Setting the colorId to -1 disables the default.
+ */
+ public void setDefaultColor (int classId, int colorId)
+ {
+ _model.setDefaultColor(classId, colorId);
+ }
+
+ /**
+ * Adds a new object to this scene.
+ */
+ public void addObject (ObjectInfo info)
+ {
+ _objects.add(info);
+
+ // add it to the underlying scene model
+ StageMisoSceneModel mmodel = StageMisoSceneModel.getSceneModel(_model);
+ if (mmodel != null) {
+ if (!mmodel.addObject(info)) {
+ Log.warning("Scene model rejected object add " +
+ "[scene=" + this + ", object=" + info + "].");
+ }
+ }
+ }
+
+ /**
+ * Removes an object from this scene.
+ */
+ public boolean removeObject (ObjectInfo info)
+ {
+ boolean removed = _objects.remove(info);
+
+ // remove it from the underlying scene model
+ StageMisoSceneModel mmodel = StageMisoSceneModel.getSceneModel(_model);
+ if (mmodel != null) {
+ removed = mmodel.removeObject(info) || removed;
+ }
+
+ return removed;
+ }
+
+ /**
+ * Iterates over all of the interesting objects in this scene.
+ */
+ public Iterator enumerateObjects ()
+ {
+ return _objects.iterator();
+ }
+
+ /**
+ * Applies the supplied scene update to this scene.
+ */
+ public void applyUpdate (SceneUpdate update)
+ {
+ if (update instanceof AddObjectUpdate) {
+ AddObjectUpdate aou = (AddObjectUpdate) update;
+
+ // remove any occluded objects
+ if (aou.casualties != null) {
+ for (int ii = 0; ii < aou.casualties.length; ii++) {
+ removeObject(aou.casualties[ii]);
+ }
+ }
+
+ // add the object
+ addObject(aou.info);
+
+ } else if (update instanceof DefaultColorUpdate) {
+ DefaultColorUpdate dcu = (DefaultColorUpdate) update;
+ setDefaultColor(dcu.classId, dcu.colorId);
+
+ } else {
+ Log.warning("Unknown scene update applied to StageScene: " + update);
+ }
+
+ // increment our scene version
+ setVersion(getVersion() + 1);
+ }
+
+ // documentation inherited
+ public Object clone ()
+ throws CloneNotSupportedException
+ {
+ // create a new scene with a clone of our model
+ return new StageScene((StageSceneModel)_model.clone(), _config);
+ }
+
+ // documentation inherited from interface
+ public Portal getPortal (int portalId)
+ {
+ return _sdelegate.getPortal(portalId);
+ }
+
+ // documentation inherited from interface
+ public int getPortalCount ()
+ {
+ return _sdelegate.getPortalCount();
+ }
+
+ // documentation inherited from interface
+ public Iterator getPortals ()
+ {
+ return _sdelegate.getPortals();
+ }
+
+ // documentation inherited from interface
+ public Portal getDefaultEntrance ()
+ {
+ return _sdelegate.getDefaultEntrance();
+ }
+
+ // documentation inherited from interface
+ public void addPortal (Portal portal)
+ {
+ _sdelegate.addPortal(portal);
+ }
+
+ // documentation inherited from interface
+ public void removePortal (Portal portal)
+ {
+ _sdelegate.removePortal(portal);
+ }
+
+ // documentation inherited from interface
+ public void setDefaultEntrance (Portal portal)
+ {
+ _sdelegate.setDefaultEntrance(portal);
+ }
+
+ /** A reference to our scene model. */
+ protected StageSceneModel _model;
+
+ /** Our spot scene delegate. */
+ protected SpotSceneImpl _sdelegate;
+
+ /** A list of all interesting scene objects. */
+ protected ArrayList _objects = new ArrayList();
+}
diff --git a/src/java/com/threerings/stage/data/StageSceneModel.java b/src/java/com/threerings/stage/data/StageSceneModel.java
new file mode 100644
index 000000000..12f3c6b8a
--- /dev/null
+++ b/src/java/com/threerings/stage/data/StageSceneModel.java
@@ -0,0 +1,85 @@
+//
+// $Id: YoSceneModel.java 17643 2004-10-28 22:58:30Z mdb $
+
+package com.threerings.stage.data;
+
+import com.threerings.util.StreamableArrayList;
+import com.threerings.util.StreamableIntIntMap;
+
+import com.threerings.whirled.data.SceneModel;
+import com.threerings.whirled.spot.data.SpotSceneModel;
+
+/**
+ * Extends the basic scene model with the notion of a scene type and
+ * incorporates the necessary auxiliary models used by the Stage system.
+ */
+public class StageSceneModel extends SceneModel
+{
+ /** A scene type code. */
+ public static final String WORLD = "world";
+
+ /** This scene's type which is a string identifier used to later
+ * construct a specific controller to handle this scene. */
+ public String type;
+
+ /** The zone id to which this scene belongs. */
+ public int zoneId;
+
+ /** If non-null, contains default colorizations to use for objects
+ * that do not have colorizations defined. */
+ public StreamableIntIntMap defaultColors;
+
+ /**
+ * Get the default color to use for the specified colorization
+ * classId, or -1 if no default is set for that colorization.
+ */
+ public int getDefaultColor (int classId)
+ {
+ if (defaultColors != null) {
+ return defaultColors.get(classId);
+ }
+ return -1;
+ }
+
+ /**
+ * Set the default colorId to use for a specified colorization
+ * classId, or -1 to clear the default for that colorization.
+ */
+ public void setDefaultColor (int classId, int colorId)
+ {
+ if (colorId == -1) {
+ if (defaultColors != null) {
+ defaultColors.remove(classId);
+ if (defaultColors.size() == 0) {
+ defaultColors = null;
+ }
+ }
+
+ } else {
+ if (defaultColors == null) {
+ defaultColors = new StreamableIntIntMap();
+ }
+ defaultColors.put(classId, colorId);
+ }
+ }
+
+ /**
+ * Creates and returns a blank scene model.
+ */
+ public static StageSceneModel blankStageSceneModel ()
+ {
+ StageSceneModel model = new StageSceneModel();
+ populateBlankStageSceneModel(model);
+ return model;
+ }
+
+ /**
+ * Populates a blank scene model with blank values.
+ */
+ protected static void populateBlankStageSceneModel (StageSceneModel model)
+ {
+ populateBlankSceneModel(model);
+ model.addAuxModel(new SpotSceneModel());
+ model.addAuxModel(new StageMisoSceneModel());
+ }
+}
diff --git a/src/java/com/threerings/stage/util/StageContext.java b/src/java/com/threerings/stage/util/StageContext.java
new file mode 100644
index 000000000..39cc6c077
--- /dev/null
+++ b/src/java/com/threerings/stage/util/StageContext.java
@@ -0,0 +1,88 @@
+//
+// $Id: BasicYoContext.java 19661 2005-03-09 02:40:29Z andrzej $
+
+package com.threerings.stage.util;
+
+import com.threerings.resource.ResourceManager;
+import com.threerings.util.KeyDispatcher;
+import com.threerings.util.KeyboardManager;
+import com.threerings.util.MessageManager;
+
+import com.threerings.media.FrameManager;
+import com.threerings.media.IconManager;
+import com.threerings.media.image.ColorPository;
+import com.threerings.media.image.ImageManager;
+import com.threerings.media.sound.SoundManager;
+
+import com.threerings.cast.CharacterManager;
+import com.threerings.cast.ComponentRepository;
+import com.threerings.miso.util.MisoContext;
+
+/**
+ * A context that provides for the myriad requirements of the Stage
+ * system.
+ */
+public interface StageContext
+ extends MisoContext
+{
+ /**
+ * Returns the frame manager driving our interface.
+ */
+ public FrameManager getFrameManager ();
+
+ /**
+ * Returns the resource manager via which all client resources are
+ * loaded.
+ */
+ public ResourceManager getResourceManager ();
+
+ /**
+ * Access to the image manager.
+ */
+ public ImageManager getImageManager ();
+
+ /**
+ * Provides access to the key dispatcher.
+ */
+ public KeyDispatcher getKeyDispatcher ();
+
+ /**
+ * Returns a reference to the message manager used by the client.
+ */
+ public MessageManager getMessageManager ();
+
+ /**
+ * Returns a reference to the icon manager used by the client.
+ */
+ public IconManager getIconManager ();
+
+ /**
+ * Returns a reference to the sound manager used by the client.
+ */
+ public SoundManager getSoundManager();
+
+ /**
+ * Returns a reference to the keyboard manager.
+ */
+ public KeyboardManager getKeyboardManager();
+
+ /**
+ * Returns the component repository in use by this client.
+ */
+ public ComponentRepository getComponentRepository ();
+
+ /**
+ * Returns a reference to the colorization repository.
+ */
+ public ColorPository getColorPository ();
+
+ /**
+ * Translates the specified message using the default bundle.
+ */
+ public String xlate (String message);
+
+ /**
+ * Translates the specified message using the specified bundle.
+ */
+ public String xlate (String bundle, String message);
+}
diff --git a/src/java/com/threerings/stage/util/StageSceneUtil.java b/src/java/com/threerings/stage/util/StageSceneUtil.java
new file mode 100644
index 000000000..3b4db7e28
--- /dev/null
+++ b/src/java/com/threerings/stage/util/StageSceneUtil.java
@@ -0,0 +1,401 @@
+//
+// $Id: YoSceneUtil.java 19769 2005-03-17 07:38:31Z mdb $
+
+package com.threerings.stage.util;
+
+import java.awt.Point;
+import java.awt.Rectangle;
+import java.util.ArrayList;
+import java.util.Comparator;
+
+import com.samskivert.util.SortableArrayList;
+import com.samskivert.util.StringUtil;
+import com.threerings.util.DirectionCodes;
+import com.threerings.util.DirectionUtil;
+
+import com.threerings.media.tile.TileManager;
+import com.threerings.media.tile.TileUtil;
+import com.threerings.media.tile.TrimmedObjectTileSet;
+import com.threerings.media.util.AStarPathUtil;
+import com.threerings.media.util.MathUtil;
+
+import com.threerings.miso.MisoConfig;
+import com.threerings.miso.data.ObjectInfo;
+import com.threerings.miso.tile.BaseTileSet;
+import com.threerings.miso.util.MisoSceneMetrics;
+import com.threerings.miso.util.MisoUtil;
+import com.threerings.miso.util.ObjectSet;
+
+import com.threerings.whirled.spot.data.Cluster;
+import com.threerings.whirled.spot.data.Location;
+import com.threerings.whirled.spot.data.SceneLocation;
+
+import com.threerings.stage.Log;
+import com.threerings.stage.data.StageMisoSceneModel;
+import com.threerings.stage.data.StageSceneModel;
+
+/**
+ * Provides scene related utility functions.
+ */
+public class StageSceneUtil
+{
+ /**
+ * Returns the scene metrics we use to do our calculations.
+ */
+ public static MisoSceneMetrics getMetrics ()
+ {
+ return _metrics;
+ }
+
+ /**
+ * Does the necessary jiggery pokery to figure out where the specified
+ * object's associated location is.
+ */
+ public static Location locationForObject (
+ TileManager tilemgr, ObjectInfo info)
+ {
+ return locationForObject(tilemgr, info.tileId, info.x, info.y);
+ }
+
+ /**
+ * Does the necessary jiggery pokery to figure out where the specified
+ * object's associated location is.
+ *
+ * @param tilemgr a tile manager that can be used to look up the tile
+ * information.
+ * @param tileId the fully qualified tile id of the object tile.
+ * @param tx the object's x tile coordinate.
+ * @param ty the object's y tile coordinate.
+ */
+ public static Location locationForObject (
+ TileManager tilemgr, int tileId, int tx, int ty)
+ {
+ try {
+ int tsid = TileUtil.getTileSetId(tileId);
+ int tidx = TileUtil.getTileIndex(tileId);
+ TrimmedObjectTileSet tset = (TrimmedObjectTileSet)
+ tilemgr.getTileSet(tsid);
+ if (tset == null || tset.getSpotOrient(tidx) < 0) {
+ return null;
+ }
+
+ Point opos = MisoUtil.tilePlusFineToFull(
+ _metrics, tx, ty, tset.getXSpot(tidx), tset.getYSpot(tidx),
+ new Point());
+
+// Log.info("Computed location [set=" + tset.getName() +
+// ", tidx=" + tidx + ", tx=" + tx + ", ty=" + ty +
+// ", sx=" + tset.getXSpot(tidx) +
+// ", sy=" + tset.getYSpot(tidx) +
+// ", lx=" + opos.x + ", ly=" + opos.y +
+// ", fg=" + _metrics.finegran + "].");
+ return new Location(opos.x, opos.y, (byte)tset.getSpotOrient(tidx));
+
+ } catch (Exception e) {
+ Log.warning("Unable to look up object tile for scene object " +
+ "[tileId=" + tileId + ", error=" + e + "].");
+ }
+ return null;
+ }
+
+ /**
+ * Converts full coordinates to Cartesian coordinates.
+ */
+ public static void locationToCoords (int lx, int ly, Point coords)
+ {
+ int tx = MisoUtil.fullToTile(lx), fx = MisoUtil.fullToFine(lx);
+ int ty = MisoUtil.fullToTile(ly), fy = MisoUtil.fullToFine(ly);
+ coords.x = tx*_metrics.finegran+fx;
+ coords.y = ty*_metrics.finegran+fy;
+ }
+
+ /**
+ * Converts Cartesian coordinates back to full coordinates.
+ */
+ public static void coordsToLocation (int cx, int cy, Point loc)
+ {
+ loc.x = MisoUtil.toFull(cx/_metrics.finegran, cx%_metrics.finegran);
+ loc.y = MisoUtil.toFull(cy/_metrics.finegran, cy%_metrics.finegran);
+ }
+
+ /**
+ * Returns the footprint, in absolute tile coordinates, for the
+ * specified object with origin as specified.
+ */
+ public static Rectangle getObjectFootprint (
+ TileManager tilemgr, int tileId, int ox, int oy)
+ {
+ Rectangle foot = new Rectangle();
+ getObjectFootprint(tilemgr, tileId, ox, oy, foot);
+ return foot;
+ }
+
+ /**
+ * Fills in the footprint, in absolute tile coordinates, for the
+ * specified object with origin as specified.
+ *
+ * @return true if the object was successfully looked up and the
+ * footprint filled in, false if an error occurred trying to look up
+ * the associated object tile.
+ */
+ public static boolean getObjectFootprint (
+ TileManager tilemgr, int tileId, int ox, int oy, Rectangle foot)
+ {
+ try {
+ int tsid = TileUtil.getTileSetId(tileId);
+ int tidx = TileUtil.getTileIndex(tileId);
+ TrimmedObjectTileSet tset = (TrimmedObjectTileSet)
+ tilemgr.getTileSet(tsid);
+ if (tset == null) {
+ return false;
+ }
+
+ int bwidth = tset.getBaseWidth(tidx);
+ int bheight = tset.getBaseHeight(tidx);
+ foot.setBounds(ox - bwidth + 1, oy - bheight + 1, bwidth, bheight);
+ return true;
+
+ } catch (Exception e) {
+ Log.warning("Unable to look up object tile for scene object " +
+ "[tileId=" + tileId + ", error=" + e + "].");
+ return false;
+ }
+ }
+
+ /**
+ * Looks up the base tile set for the specified fully qualified tile
+ * identifier and returns true if the associated tile is passable.
+ */
+ public static boolean isPassable (TileManager tilemgr, int tileId)
+ {
+ // non-existent tiles are not passable
+ if (tileId <= 0) {
+ return false;
+ }
+
+ try {
+ int tsid = TileUtil.getTileSetId(tileId);
+ int tidx = TileUtil.getTileIndex(tileId);
+ BaseTileSet tset = (BaseTileSet)tilemgr.getTileSet(tsid);
+ return tset.getPassability()[tidx];
+
+ } catch (Exception e) {
+ Log.warning("Unable to look up base tile [tileId=" + tileId +
+ ", error=" + e + "].");
+ return true;
+ }
+ }
+
+ /**
+ * Computes a list of the valid locations in this cluster.
+ */
+ public static ArrayList getClusterLocs (Cluster cluster)
+ {
+ ArrayList list = new ArrayList();
+
+ // convert our tile coordinates into a cartesian coordinate system
+ // with units equal to one fine coordinate in size
+ int fx = cluster.x*_metrics.finegran+1,
+ fy = cluster.y*_metrics.finegran+1;
+ int fwid = cluster.width*_metrics.finegran-2,
+ fhei = cluster.height*_metrics.finegran-2;
+ int cx = fx + fwid/2, cy = fy + fhei/2;
+
+ // if it's a 1x1 cluster, return one location in the center of the
+ // cluster
+ if (cluster.width == 1) {
+ list.add(new SceneLocation(MisoUtil.toFull(cluster.x, 2),
+ MisoUtil.toFull(cluster.y, 2),
+ (byte)DirectionCodes.SOUTHWEST, 0));
+ return list;
+ }
+
+ double radius = (double)fwid/2;
+ int clidx = cluster.width-2;
+ if (clidx >= CLUSTER_METRICS.length/2 || clidx < 0) {
+ Log.warning("Requested locs from invalid cluster " + cluster + ".");
+ Thread.dumpStack();
+ return list;
+ }
+
+ for (double angle = CLUSTER_METRICS[clidx*2]; angle < Math.PI*2;
+ angle += CLUSTER_METRICS[clidx*2+1]) {
+ int sx = cx + (int)Math.round(Math.cos(angle) * radius);
+ int sy = cy + (int)Math.round(Math.sin(angle) * radius);
+
+ // obtain the orientation facing toward the center
+ int orient = 2*(int)(Math.round(angle/(Math.PI/4))%8);
+ orient = DirectionUtil.rotateCW(DirectionCodes.SOUTH, orient);
+ orient = DirectionUtil.getOpposite(orient);
+
+ // convert them back to full coordinates for the location
+ int tx = MathUtil.floorDiv(sx, _metrics.finegran);
+ sx = MisoUtil.toFull(tx, sx-(tx*_metrics.finegran));
+ int ty = MathUtil.floorDiv(sy, _metrics.finegran);
+ sy = MisoUtil.toFull(ty, sy-(ty*_metrics.finegran));
+ list.add(new SceneLocation(sx, sy, (byte)orient, 0));
+ }
+
+ return list;
+ }
+
+// /**
+// * Returns true if this user is available to be clustered with.
+// */
+// public static boolean isClusterable (YoOccupantInfo info)
+// {
+// switch (info.activity) {
+// case ActivityCodes.NONE:
+// case ActivityCodes.READING:
+// case ActivityCodes.IDLE:
+// case ActivityCodes.DISCONNECTED:
+// return true;
+// default:
+// return false;
+// }
+// }
+
+ /**
+ * Locates a spot to stand near the supplied rectangular footprint.
+ * First a spot will be sought in a tile immediately next to the
+ * footprint, then one tile removed, then two, up to the maximum
+ * distance specified by dist.
+ *
+ * @param foot the tile coordinate footprint around which we are
+ * attempting to stand.
+ * @param dist the maximum number of tiles away from the footprint to
+ * search before giving up.
+ * @param pred a predicate that will be used to determine whether a
+ * particular spot can be stood upon (we're hijacking the meaning of
+ * "traverse" in this case, but the interface is otherwise so nice).
+ * @param traverser the object that will be passed to the traversal
+ * predicate.
+ * @param nearto a point (in tile coordinates) which will be used to
+ * select from among the valid standing spots, the one nearest to the
+ * supplied point will be returned.
+ * @param orient if not {@link DirectionCodes#NONE} this orientation
+ * will be used to override the "natural" orientation of the spot
+ * which is facing toward the footprint.
+ *
+ * @return the closest spot to the
+ */
+ public static Location findStandingSpot (
+ Rectangle foot, int dist, AStarPathUtil.TraversalPred pred,
+ Object traverser, final Point nearto, int orient)
+ {
+ // generate a list of the tile coordinates of all squares around
+ // this footprint
+ SortableArrayList spots = new SortableArrayList();
+
+ for (int dd = 1; dd <= dist; dd++) {
+ int yy1 = foot.y-dd, yy2 = foot.y+foot.height+dd-1;
+ int xx1 = foot.x-dd, xx2 = foot.x+foot.width+dd-1;
+
+ // get the corners
+ spots.add(new Location(xx1, yy1, (byte)DirectionCodes.SOUTHWEST));
+ spots.add(new Location(xx1, yy2, (byte)DirectionCodes.SOUTHEAST));
+ spots.add(new Location(xx2, yy1, (byte)DirectionCodes.NORTHWEST));
+ spots.add(new Location(xx2, yy2, (byte)DirectionCodes.NORTHEAST));
+
+ // then the sides
+ for (int xx = xx1+1; xx < xx2; xx++) {
+ spots.add(new Location(xx, yy1, (byte)DirectionCodes.WEST));
+ spots.add(new Location(xx, yy2, (byte)DirectionCodes.EAST));
+ }
+ for (int yy = yy1+1; yy < yy2; yy++) {
+ spots.add(new Location(xx1, yy, (byte)DirectionCodes.SOUTH));
+ spots.add(new Location(xx2, yy, (byte)DirectionCodes.NORTH));
+ }
+
+ // sort them in order of closeness to the players current
+ // coordinate
+ spots.sort(new Comparator() {
+ public int compare (Object o1, Object o2) {
+ return dist((Location)o1) - dist((Location)o2);
+ }
+ private final int dist (Location l) {
+ return Math.round(100*MathUtil.distance(
+ l.x, l.y, nearto.x, nearto.y));
+ }
+ });
+
+ // return the first spot that can be "traversed" which we're
+ // taking to mean "stood upon"
+ for (int ii = 0, ll = spots.size(); ii < ll; ii++) {
+ Location loc = (Location)spots.get(ii);
+ if (pred.canTraverse(traverser, loc.x, loc.y)) {
+ // convert to full coordinates
+ loc.x = MisoUtil.toFull(loc.x, 2);
+ loc.y = MisoUtil.toFull(loc.y, 2);
+
+ // see if we need to override the orientation
+ if (DirectionCodes.NONE != orient) {
+ loc.orient = (byte) orient;
+ }
+ return loc;
+ }
+ }
+
+ // clear this list and try one further out
+ spots.clear();
+ }
+
+ return null;
+ }
+
+ /**
+ * Returns an array of the objects intersected by the supplied tile
+ * coordinate rectangle.
+ */
+ public static ObjectInfo[] getIntersectedObjects (
+ TileManager tmgr, StageSceneModel model, Rectangle rect)
+ {
+ // first get all objects whose origin is in an expanded version of
+ // our intersection rect, any object that is *so* large that its
+ // origin falls outside of this rectangle but that still
+ // intersects this rectangle can go to hell; it's either this or
+ // we iterate over every object in the whole goddamned scene which
+ // is so hairily inefficient i can't even bear to contemplate it
+ ObjectSet objs = new ObjectSet();
+ Rectangle orect = new Rectangle(rect);
+ orect.grow(MAX_OBJECT_SIZE, MAX_OBJECT_SIZE);
+ StageMisoSceneModel mmodel = StageMisoSceneModel.getSceneModel(model);
+ mmodel.getObjects(orect, objs);
+
+ // now prune from this set any and all objects that don't actually
+ // overlap the specified rectangle
+ Rectangle foot = new Rectangle();
+ for (int ii = 0; ii < objs.size(); ii++) {
+ ObjectInfo info = objs.get(ii);
+ if (getObjectFootprint(tmgr, info.tileId, info.x, info.y, foot)) {
+ if (!foot.intersects(rect)) {
+ objs.remove(ii--);
+ }
+ } else {
+ Log.warning("Unknown potentially intersecting object?! " +
+ "[scene=" + model.name + " (" + model.sceneId +
+ "), info=" + info + "].");
+ }
+ }
+
+ return objs.toArray();
+ }
+
+ /** Our default scene metrics. */
+ protected static MisoSceneMetrics _metrics = MisoConfig.getSceneMetrics();
+
+ /** Contains the starting offset from zero radians for the first
+ * occupant and the radial distance between occupants. */
+ protected static final double[] CLUSTER_METRICS = {
+ Math.PI/4, Math.PI/2, // 2x
+ Math.PI/4, Math.PI/2, // 3x
+ 0, Math.PI/4, // 4x
+ Math.PI/12, Math.PI/6, // 5x
+ 0, Math.PI/8, // 6x
+ Math.PI/24, Math.PI/12, // 7x
+ };
+
+ /** The maximum footprint width or height for which we will account in
+ * {@link #getIntersectedObjects}. */
+ protected static final int MAX_OBJECT_SIZE = 15;
+}