7a72ac8fb2
git-svn-id: svn+ssh://src.earth.threerings.net/nenya/trunk@984 ed5b42cb-e716-0410-a449-f6a68f950b19
619 lines
20 KiB
ActionScript
619 lines
20 KiB
ActionScript
//
|
|
// Nenya library - tools for developing networked games
|
|
// Copyright (C) 2002-2010 Three Rings Design, Inc., All Rights Reserved
|
|
// http://www.threerings.net/code/nenya/
|
|
//
|
|
// This library is free software; you can redistribute it and/or modify it
|
|
// under the terms of the GNU Lesser General Public License as published
|
|
// by the Free Software Foundation; either version 2.1 of the License, or
|
|
// (at your option) any later version.
|
|
//
|
|
// This library is distributed in the hope that it will be useful,
|
|
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
|
|
// Lesser General Public License for more details.
|
|
//
|
|
// You should have received a copy of the GNU Lesser General Public
|
|
// License along with this library; if not, write to the Free Software
|
|
// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
|
|
|
|
package com.threerings.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.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.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);
|
|
|
|
_objScene = new IsoScene();
|
|
_objScene.layoutRenderer = new ClassFactory(PrioritizedSceneLayoutRenderer);
|
|
|
|
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) + "%";
|
|
};
|
|
_loadingProgressFunc(0.0);
|
|
return loadingText;
|
|
}
|
|
|
|
protected function loadingProgress (progress :Number) :void
|
|
{
|
|
if (_loadingProgressFunc != null) {
|
|
_loadingProgressFunc(progress);
|
|
}
|
|
}
|
|
|
|
public function onClick (event :MouseEvent) :void
|
|
{
|
|
// TODO - handle _hobject
|
|
handleMousePressed(null, event);
|
|
}
|
|
|
|
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();
|
|
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.x/2 : 0);
|
|
var maxY :int = BOTTOM_BUFFER + size.y + yMove + (buffer ? size.x/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 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);
|
|
|
|
// Force to end of list so it's on top...
|
|
_isoView.removeScene(_objScene);
|
|
_isoView.addScene(_objScene);
|
|
|
|
_objScene.render();
|
|
|
|
// 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
|
|
|
|
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;
|
|
} |