Checkpoint of "work as is".
authornotzed@gmail.com <notzed@gmail.com@b8b59bfb-1aa4-4687-8f88-a62eeb14c21e>
Sun, 15 Sep 2013 00:09:58 +0000 (00:09 +0000)
committernotzed@gmail.com <notzed@gmail.com@b8b59bfb-1aa4-4687-8f88-a62eeb14c21e>
Sun, 15 Sep 2013 00:09:58 +0000 (00:09 +0000)
But I did add tile animations.

git-svn-id: file:///home/notzed/svn/duskz/trunk@14 b8b59bfb-1aa4-4687-8f88-a62eeb14c21e

DuskZ/README
DuskZ/src/duskz/client/ClientMap.java
DuskZ/src/duskz/client/Dusk.java
DuskZ/src/duskz/client/Entity.java
DuskZ/src/duskz/client/Equipment.java
DuskZ/src/duskz/client/Status.java
DuskZ/src/duskz/client/fx/DataManagerFX.java
DuskZ/src/duskz/client/fx/EquipmentPane.java
DuskZ/src/duskz/client/fx/MainFrameFX.java
DuskZ/src/duskz/client/fx/TileAnimator.java [new file with mode: 0644]

index 5ab7274..51bd46a 100644 (file)
@@ -1,4 +1,16 @@
 
+CODE STATUS 11/13
+-----------------
+
+As most of the 'smarts' in Dusk happens in the server the client is
+fairly simple - it basically handles some local input and rendering
+the tiles.
+
+This should be feature complete against the latest DuskServer, but
+needs some aesthetic and usability work.
+
+Original README follows.
+
 README
 ------
 This is the client frontend to DuskZ, it uses JavaFX.  It connects
@@ -9,7 +21,7 @@ circa 2000.
 
 Currently a recent Oracle JRE is required to execute this application.
 
-This is currently in an alpha state and in very active development.
+This is currently in an alpha state.
 
  ... to be completed ...
 
@@ -42,3 +54,33 @@ LICENSE
 
   You should have received a copy of the GNU General Public License
   along with DuskZ.  If not, see <http://www.gnu.org/licenses/>.
+
+
+  DuskZ also uses the ListSpinner widget from JFXExtras.
+
+/**
+ * Copyright (c) 2011, JFXtras
+ * All rights reserved.
+ * 
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *     * Redistributions of source code must retain the above copyright
+ *       notice, this list of conditions and the following disclaimer.
+ *     * Redistributions in binary form must reproduce the above copyright
+ *       notice, this list of conditions and the following disclaimer in the
+ *       documentation and/or other materials provided with the distribution.
+ *     * Neither the name of the <organization> nor the
+ *       names of its contributors may be used to endorse or promote products
+ *       derived from this software without specific prior written permission.
+ * 
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
+ * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+ * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL <COPYRIGHT HOLDER> BE LIABLE FOR ANY
+ * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+ * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+ * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
+ * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
\ No newline at end of file
index 7e4b131..8856b24 100644 (file)
@@ -149,7 +149,7 @@ public class ClientMap {
 
        public synchronized void addEntity(Entity e) {
                if (entityByID.containsKey(e.ID)) {
-                       throw new RuntimeException("entity already in table " + e.ID + " " + e.strName);
+                       throw new RuntimeException("entity already in table " + e.ID + " " + e.name);
                }
 
                entityByID.put(e.ID, e);
index 1a9d237..fbc0264 100644 (file)
@@ -35,8 +35,6 @@ package duskz.client;
 
 import duskz.protocol.*;
 import duskz.protocol.DuskMessage.*;
-import static duskz.protocol.DuskProtocol.FIELD_AUTH_NEWPLAYER;
-import static duskz.protocol.DuskProtocol.FIELD_AUTH_REASON;
 import java.io.*;
 import java.net.*;
 import java.util.ArrayList;
@@ -194,12 +192,12 @@ public class Dusk implements Runnable, DuskProtocol {
        /**
         * FIXME: all needs a rewrite
         *
-        * @param entStore
+        * @param e
         */
        // add the thing, this renumbers any that have the same name
-       void addEntity(Entity entStore) {
-               System.out.println("Add entity: " + entStore.ID + " " + entStore.strName);
-               map.addEntity(entStore);
+       void addEntity(Entity e) {
+               System.out.println("Add entity: " + e.ID + " " + e.name + " " + e.locx + "x" + e.locy);
+               map.addEntity(e);
 
                // FIXME: call rement if it was out of range so the server knows
        }
@@ -302,7 +300,7 @@ public class Dusk implements Runnable, DuskProtocol {
                                        case MSG_CLEAR_FLAGS:
                                                synchronized (map) {
                                                        for (Entity e : map.getEntities()) {
-                                                               e.intFlag = 0;
+                                                               e.flags = 0;
                                                        }
                                                }
                                                update();
@@ -334,7 +332,7 @@ public class Dusk implements Runnable, DuskProtocol {
                                                //frame.info.setText("HP: " + inthp + "/" + intmaxhp + " MP: " + intsp + "/" + intmaxsp + " Loc: " + LocX + "/" + LocY);
                                                buyList.clear();
                                                reloadChoiceLookGetAttack();
-                                               
+
                                                // Perhaps i need a status message to go from 'starting' to 'ready'
                                                loaded = true;
                                                break;
@@ -356,7 +354,10 @@ public class Dusk implements Runnable, DuskProtocol {
                                                        for (DuskMessage m : el.value) {
                                                                switch (m.name) {
                                                                        case FIELD_ENTITY_FLAGS:
-                                                                               e.intFlag = ((IntegerMessage) m).value;
+                                                                               e.flags = ((IntegerMessage) m).value;
+                                                                               break;
+                                                                       case FIELD_ENTITY_CONDITIONS:
+                                                                               e.conditions = ((StringListMessage) m).value;
                                                                                break;
                                                                }
                                                        }
@@ -519,7 +520,7 @@ public class Dusk implements Runnable, DuskProtocol {
                                e.intMoveDirection = dir;
                        }
                }
-               reloadChoiceAttack();
+               reloadChoiceLookGetAttack();
                // TODO: find out what this is for
                //if (addit) {
                //      thrGraphics.addEntityToMove(ent, dir);
@@ -557,7 +558,6 @@ public class Dusk implements Runnable, DuskProtocol {
                nextQuery();
        }
 
-       // TBD - use the individual functions
        public void reloadChoiceLookGetAttack() {
                final ArrayList<Entity> looks = new ArrayList<>();
                final ArrayList<Entity> gets = new ArrayList<>();
@@ -581,42 +581,6 @@ public class Dusk implements Runnable, DuskProtocol {
                frame.setTakeList(gets);
        }
 
-       public void reloadChoiceLook() {
-               final ArrayList<Entity> looks = new ArrayList<>();
-
-               synchronized (map) {
-                       for (Entity e : map.getEntities()) {
-                               if (status.canLook(e))
-                                       looks.add(e);
-                       }
-               }
-               frame.setLookList(looks);
-       }
-
-       public void reloadChoiceAttack() {
-               final ArrayList<Entity> attacks = new ArrayList<>();
-
-               synchronized (map) {
-                       for (Entity e : map.getEntities()) {
-                               if (status.canAttack(e))
-                                       attacks.add(e);
-                       }
-               }
-               frame.setAttackList(attacks);
-       }
-
-       public void reloadChoiceGet() {
-               final ArrayList<Entity> gets = new ArrayList<>();
-
-               synchronized (map) {
-                       for (Entity e : map.getEntities()) {
-                               if (status.canTake(e))
-                                       gets.add(e);
-                       }
-               }
-               frame.setTakeList(gets);
-       }
-
        public boolean isConnected() {
                return connected;
        }
@@ -739,8 +703,15 @@ public class Dusk implements Runnable, DuskProtocol {
                docmd(what);
        }
 
-       public void command(String what, String params) {
-               docmd(what + " " + params);
+       public void command(String what, String... params) {
+               StringBuilder sb = new StringBuilder(what);
+               for (String s : params) {
+                       sb.append(' ');
+                       sb.append('"');
+                       sb.append(s);
+                       sb.append('"');
+               }
+               docmd(sb.toString());
        }
 
        public void move(Direction dir) {
@@ -757,11 +728,11 @@ public class Dusk implements Runnable, DuskProtocol {
        }
 
        public void buy(TransactionItem item, int quantity) {
-               command("buy " + quantity + " " + item.name);
+               command("buy", String.valueOf(quantity), item.name);
        }
 
        public void sell(TransactionItem item, int quantity) {
-               command("sell " + quantity + " " + item.name);
+               command("sell", String.valueOf(quantity), item.name);
        }
 
        private void ping() {
@@ -781,14 +752,14 @@ public class Dusk implements Runnable, DuskProtocol {
        }
 
        public void drop(String what) {
-               command("drop " + what);
+               command("drop", what);
        }
 
        public void wear(String what) {
-               command("wear " + what);
+               command("wear", what);
        }
 
        public void unwear(String what) {
-               command("unwear " + what);
+               command("unwear", what);
        }
 }
index 2e3bdbd..2fa519f 100644 (file)
  */
 package duskz.client;
 
+import duskz.protocol.DuskProtocol;
 import duskz.protocol.EntityUpdateMessage;
+import java.util.ArrayList;
+import java.util.List;
 
 public class Entity {
 
-       public String strName;
+       public String name;
        public int locx,
                        locy;
        public int intType,
@@ -43,8 +46,9 @@ public class Entity {
                         2 west
                         3 east
                         */
-                       intFlag = 0;
-       /*intFlag
+                       flags = 0;
+       List<String> conditions = new ArrayList<>(0);
+       /*flags
         0 none
         1 ally
         2 enemy
@@ -53,7 +57,7 @@ public class Entity {
        Entity entNext = null;
 
        public Entity(String instrName, long inID, int inintLocX, int inintLocY, int inImage, int inStep, int inintType) {
-               strName = instrName;
+               name = instrName;
                ID = inID;
                locx = inintLocX;
                locy = inintLocY;
@@ -63,7 +67,7 @@ public class Entity {
        }
 
        public Entity(EntityUpdateMessage msg) {
-               strName = msg.entityName;
+               name = msg.entityName;
                locx = msg.x;
                locy = msg.y;
                ID = msg.id;
@@ -78,21 +82,49 @@ public class Entity {
         * @return
         */
        public String getSimpleName() {
-               int i = strName.lastIndexOf(">");
+               int i = name.lastIndexOf(">");
                if (i != -1) {
-                       return strName.substring(i + 1);
+                       return name.substring(i + 1);
                } else {
-                       return strName;
+                       return name;
                }
        }
 
        public String getIndexedName() {
-               return intNum == 0 ? strName : intNum + "." + strName;
+               return intNum == 0 ? name : intNum + "." + name;
        }
 
        @Override
        public String toString() {
 
-               return "[Entity " + ID + ", " + strName + ", " + locx + ", " + locy + "]";
+               return "[Entity " + ID + ", " + name + ", " + locx + ", " + locy + "]";
+       }
+
+       public String getTitle() {
+               if (conditions.isEmpty() && flags == 0)
+                       return name;
+
+               StringBuilder sb = new StringBuilder(name);
+
+               sb.append('<');
+               boolean first = true;
+               for (String c : conditions) {
+                       if (!first) {
+                               sb.append(", ");
+                       }
+                       first = false;
+                       sb.append(c);
+               }
+
+               if ((flags & DuskProtocol.ENTITY_FLAG_SLEEPING) != 0) {
+                       if (!first) {
+                               sb.append(", ");
+                       }
+                       sb.append("sleeping");
+               }
+
+               sb.append('>');
+
+               return sb.toString();
        }
 }
index 7472473..d63476c 100644 (file)
@@ -25,7 +25,7 @@ import duskz.protocol.DuskMessage;
 import duskz.protocol.DuskMessage.StringMessage;
 import duskz.protocol.ListMessage;
 import duskz.protocol.TransactionItem;
-import duskz.protocol.Wearing;
+import static duskz.protocol.Wearing.*;
 import java.util.ArrayList;
 import java.util.HashMap;
 import java.util.List;
@@ -36,7 +36,7 @@ import java.util.List;
  *
  * @author notzed
  */
-public class Equipment implements Wearing {
+public class Equipment  {
 
        /**
         * Worn items
@@ -102,7 +102,7 @@ public class Equipment implements Wearing {
                        StringMessage sm = (StringMessage) dm;
                        
                        // remap to local id;
-                       int i = sm.name - 1;
+                       int i = sm.name; //sm.name - 1;
                        if (i >= 0 && i < available.length) {
                                available[i].add(sm.value);
                                wearableAt.put(sm.value, i);
index 35d4ce9..c87b162 100644 (file)
@@ -92,8 +92,11 @@ public class Status implements DuskProtocol {
        }
 
        public boolean canAttack(Entity e) {
+               // Ugh, i'm not sure what the types were, but i think it just meant 'any living thing'.
+               // Dunno why the server doesn't send this message to the client anyway
                return e.ID != id
-                               && ((e.intType == 0 || e.intType == 1 || e.intType == 4)
+//                             && ((e.intType == 0 || e.intType == 1 || e.intType == 4)
+                               && ((e.intType == 0 || e.intType == 1 || e.intType == 2)
                                && (distance(e) <= range));
        }
 }
index 5720ed2..041ee6c 100644 (file)
@@ -45,6 +45,21 @@ public class DataManagerFX extends DataManager {
                return new ImageSetFX();
        }
 
+       public void updateTile(ImageView iv, int tileid, int tilewidth, int tileheight) {
+               for (int i = 0; i < tilesets.length; i++) {
+                       ImageSetFX is = (ImageSetFX) tilesets[i];
+                       if (tileid >= is.gid && tileid < is.gid + is.count) {
+                               tileid -= is.gid;
+                               iv.setImage(is.image);
+                               //iv.setViewport(new Rectangle2D(tileid * is.width, 0, is.width, is.height));
+                               iv.setViewport(is.getViewport(tileid));
+                               iv.setTranslateY(-(is.height - tileheight));
+                               //iv.relocate(tilex * tilewidth, tiley * tileheight - (is.height - tileheight));
+                               return;
+                       }
+               }
+       }
+
        /**
         * Create a tile image and align it to the baseline of the map tile.
         *
@@ -65,7 +80,8 @@ public class DataManagerFX extends DataManager {
                                        ImageView iv = new ImageView(is.image);
 
                                        tileid -= is.gid;
-                                       iv.setViewport(new Rectangle2D(tileid * is.width, 0, is.width, is.height));
+                                       //iv.setViewport(new Rectangle2D(tileid * is.width, 0, is.width, is.height));
+                                       iv.setViewport(is.getViewport(tileid));
                                        iv.relocate(tilex * tilewidth, tiley * tileheight - (is.height - tileheight));
 
                                        return iv;
@@ -82,6 +98,7 @@ public class DataManagerFX extends DataManager {
        public class ImageSetFX extends ImageSet {
 
                Image image;
+               Rectangle2D tiles[];
 
                public ImageSetFX() {
                }
@@ -91,10 +108,19 @@ public class DataManagerFX extends DataManager {
                        try (InputStream s = getInputStream()) {
                                image = new Image(s);
                        }
+
+                       tiles = new Rectangle2D[count];
+                       for (int i = 0; i < count; i++) {
+                               tiles[i] = new Rectangle2D(i * width, 0, width, height);
+                       }
                }
 
                public Image getImage() {
                        return image;
                }
+
+               public Rectangle2D getViewport(int tileid) {
+                       return tiles[tileid];
+               }
        }
 }
index f515d48..b57b98b 100644 (file)
@@ -22,6 +22,7 @@
 package duskz.client.fx;
 
 import duskz.client.Equipment;
+import duskz.protocol.Wearing;
 import java.util.ArrayList;
 import java.util.List;
 import javafx.beans.value.ChangeListener;
@@ -127,7 +128,7 @@ public class EquipmentPane extends HBox {
 
                @Override
                public String toString() {
-                       return wornAt == -1 ? name : name + " [worn: " + Equipment.titles[wornAt] + "]";
+                       return wornAt == -1 ? name : name + " [worn: " + Wearing.wornTitles[wornAt] + "]";
                }
        }
 
index cf7a40f..c82dc54 100644 (file)
@@ -35,6 +35,7 @@ import duskz.client.Equipment;
 import duskz.client.GUI;
 import duskz.client.Status;
 import duskz.protocol.TransactionItem;
+import duskz.protocol.Wearing;
 import java.io.FileNotFoundException;
 import java.io.IOException;
 import java.util.ArrayList;
@@ -43,6 +44,7 @@ import java.util.List;
 import java.util.Random;
 import java.util.logging.Level;
 import java.util.logging.Logger;
+import javafx.animation.Animation;
 import javafx.animation.FadeTransition;
 import javafx.animation.FadeTransitionBuilder;
 import javafx.animation.Interpolator;
@@ -621,7 +623,7 @@ public class MainFrameFX extends StackPane implements GUI {
                equip.unwear.setOnAction(new EventHandler<ActionEvent>() {
                        public void handle(ActionEvent t) {
                                EquipmentPane.ItemInfo ii = equip.getItem();
-                               game.unwear(Equipment.names[ii.wornAt]);
+                               game.unwear(Wearing.wornNames[ii.wornAt]);
                        }
                });
                equip.drop.setOnAction(new EventHandler<ActionEvent>() {
@@ -654,7 +656,7 @@ public class MainFrameFX extends StackPane implements GUI {
        public void visitFile(String file, String text, boolean canSave) {
                throw new UnsupportedOperationException("Not supported yet.");
                // FIXME: implement edit text, 
-               // FIXME: sent using: appParent.outstream.writeBytes("submit "+strName+"\n");
+               // FIXME: sent using: appParent.outstream.writeBytes("submit "+name+"\n");
                //                    appParent.outstream.writeBytes(txtEdit.getText()+"\n--EOF--\n");
        }
        //Accept key input
@@ -886,6 +888,7 @@ public class MainFrameFX extends StackPane implements GUI {
                        Logger.getLogger(MainFrameFX.class.getName()).log(Level.SEVERE, null, ex);
                }
        }
+       TileAnimator animator;
 
        @Override
        public void updateMap(ClientMap map) {
@@ -893,6 +896,8 @@ public class MainFrameFX extends StackPane implements GUI {
 
                // Since we get a whole update for the map every time, there isn't much we
                // can practically do apart from simply build a whole new page to display.
+               // However ... this is jused for every type of changed thing, and that
+               // isn't neccessary.
                System.out.println("update map");
                if (data == null) {
                        //if (tileImage == null) {
@@ -904,17 +909,27 @@ public class MainFrameFX extends StackPane implements GUI {
                final ArrayList<Node> upper = new ArrayList<>();
                int levelCount = map.getLevelCount();
 
-               // Build map
+               // Animated tiles hack test
+               final Rectangle2D[] anims = new Rectangle2D[2];
+               anims[0] = data.createTile(305, 0, 0, tileSize, tileSize).getViewport();
+               anims[1] = data.createTile(304, 0, 0, tileSize, tileSize).getViewport();
+               final List<ImageView> animated = new ArrayList<>();
+
+               // Build map            
                for (int l = 0; l < levelCount; l++) {
                        for (int y = 0; y < map.rows; y++) {
                                // Draw tiles first for whole row
                                for (int x = 0; x < map.cols; x++) {
                                        int tileid = map.getTile(l, x, y);
                                        if (tileid != 0) {
-                                               children.add(data.createTile(tileid, x, y, tileSize, tileSize));
+                                               ImageView iv = data.createTile(tileid, x, y, tileSize, tileSize);
+                                               children.add(iv);
+
+                                               if (tileid == 305)
+                                                       animated.add(iv);
                                        }
                                }
-                               
+
                                // Now check for entities over this layer row
                                if (l == map.getGroundLevel()) {
                                        for (int x = 0; x < map.cols; x++) {
@@ -939,6 +954,14 @@ public class MainFrameFX extends StackPane implements GUI {
                        public void run() {
                                graphics.getChildren().setAll(children);
                                graphics.getChildren().addAll(upper);
+
+                               System.out.println("animated node count = " + animated.size());
+                               if (animator == null) {
+                                       animator = new TileAnimator(animated, Duration.seconds(0.25), anims);
+                                       animator.setCycleCount(Animation.INDEFINITE);
+                                       animator.play();
+                               } else
+                                       animator.setNodes(animated);
                        }
                });
        }
@@ -965,28 +988,29 @@ public class MainFrameFX extends StackPane implements GUI {
                        ImageView iv = new ImageView(playerImage);
                        iv.setViewport(new Rectangle2D((e.intImage * 8 + e.intStep) * spriteSize, 0,
                                        spriteSize, spriteSize));
-                       iv.relocate((x * tileSize) - tileSize / 2, (y * tileSize) - tileSize / 2);
-                       iv.setScaleX(0.5);
-                       iv.setScaleY(0.5);
+                       iv.relocate((x * tileSize) + tileSize / 2 - spriteSize / 2, (y * tileSize) - spriteSize / 2);
+                       iv.setScaleX(1);
+                       iv.setScaleY(1);
                        children.add(iv);
                }
+               // FIXME: intnum not used anymore
                if (e.intNum == 0) {
-                       Text t = new Text(e.strName);
+                       Text t = new Text(e.getTitle());
                        t.setId("entity-label");
                        t.relocate((x * tileSize) + tileSize / 2 - t.getLayoutBounds().getWidth() / 2, ((y + 1) * tileSize));
                        upper.add(t);
                } else {
-                       Text t = new Text(e.intNum + "." + e.strName);
+                       Text t = new Text(e.intNum + "." + e.name);
                        t.setId("entity-label");
                        t.relocate((x * tileSize) + tileSize / 2 - t.getLayoutBounds().getWidth() / 2, ((y + 1) * tileSize));
                        upper.add(t);
                }
                //Draw flag
-               if (e.intFlag != 0) {
+               if (e.flags != 0) {
                        Rectangle r = new Rectangle(1, 1, tileSize - 2, tileSize - 2);
-                       if (e.intFlag == 1) {
+                       if ((e.flags & 3) == 1) {
                                r.setStroke(Color.GREEN);
-                       } else if (e.intFlag == 2) {
+                       } else if ((e.flags & 3) == 2) {
                                r.setStroke(Color.RED);
                        }
                        r.setStrokeWidth(2);
diff --git a/DuskZ/src/duskz/client/fx/TileAnimator.java b/DuskZ/src/duskz/client/fx/TileAnimator.java
new file mode 100644 (file)
index 0000000..6a73323
--- /dev/null
@@ -0,0 +1,54 @@
+/*
+ * This file is part of DuskZ, a graphical mud engine.
+ *
+ * Copyright (C) 2013 Michael Zucchi <notzed@gmail.com>
+ *
+ * DuskZ is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * DuskZ 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 General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with DuskZ.  If not, see <http://www.gnu.org/licenses/>.
+ */
+package duskz.client.fx;
+
+import java.util.List;
+import javafx.animation.Transition;
+import javafx.geometry.Rectangle2D;
+import javafx.scene.image.ImageView;
+import javafx.util.Duration;
+
+/**
+ * Animates a set of tiles by switching the viewport on the texture.
+ *
+ * @author Michael Zucchi <notzed@gmail.com>
+ */
+public class TileAnimator extends Transition {
+
+       Rectangle2D[] viewports;
+       List<ImageView> nodes;
+
+       public TileAnimator(List<ImageView> nodes, Duration duration, Rectangle2D[] images) {
+               setCycleDuration(duration);
+               this.viewports = images;
+               this.nodes = nodes;
+       }
+
+       public void setNodes(List<ImageView> nodes) {
+               this.nodes = nodes;
+       }
+
+       @Override
+       protected void interpolate(double d) {
+               int index = Math.min(viewports.length - 1, (int) (d * viewports.length));
+
+               for (ImageView node : nodes)
+                       node.setViewport(viewports[index]);
+       }
+}