Commits

Anonymous committed f1b67c6

Basic game functionality added.

Comments (0)

Files changed (29)

defender/src/main/java/com/fxexperience/games/defender/DefenderApp.java

         log.info("JavaFX Tower Defender started successfully - ready for action");
 
         Game game = new Game(mainView);
-        game.showNewGameDialog();
+        game.showNewGameView();
     }
 }

defender/src/main/java/com/fxexperience/games/defender/Game.java

 package com.fxexperience.games.defender;
 
 import com.fxexperience.games.defender.level.Level;
-import com.fxexperience.games.defender.level.LevelFactory;
-import com.fxexperience.games.defender.level.SimpleLevel;
-import com.fxexperience.games.defender.ui.DefenderMainView;
-import com.fxexperience.games.defender.ui.GameView;
-import com.fxexperience.games.defender.ui.LevelCompleteView;
-import com.fxexperience.games.defender.ui.NewGameView;
+import com.fxexperience.games.defender.tower.Tower;
+import com.fxexperience.games.defender.tower.TowerFactory;
+import com.fxexperience.games.defender.ui.*;
 import javafx.beans.property.BooleanProperty;
 import javafx.beans.property.ObjectProperty;
 import javafx.beans.property.SimpleBooleanProperty;
 import javafx.beans.property.SimpleObjectProperty;
 import javafx.beans.value.ChangeListener;
 import javafx.beans.value.ObservableValue;
+import javafx.event.EventHandler;
+import javafx.fxml.FXMLLoader;
+import javafx.scene.input.MouseEvent;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
+import java.io.IOException;
+import java.io.InputStream;
+
 public class Game {
 
     private static final Logger log = LoggerFactory.getLogger(Game.class);
 
+    // this could be loaded from a game definition file if wanted
+    private static final String[] LEVELS = new String[] {
+            "/fxml/level/level-1.fxml",
+            "/fxml/level/level-2.fxml",
+            "/fxml/level/level-3.fxml"
+    };
+
+    private Player player;
+
+    private int nextLevel = 0;
+
     private final DefenderMainView defenderMainView;
     private GameView gameView;
-    private ObjectProperty<Level> level;
+    private NewGameView newGameView;
+    private GameWonView gameWonView;
+    private GameLostView gameLostView;
+    private LevelCompleteView levelCompleteView;
+
     private ObjectProperty<Difficulty> difficulty;
     private BooleanProperty paused;
-    private int nextLevel = 1;
-    private LevelCompleteListener levelCompleteListener;
-    private NewGameView newGameView;
-    private Level currentLevel;
+    private ObjectProperty<Level> level;
+    private ObjectProperty<TowerFactory> selectedTowerFactory;
+
+    private LevelCompleteHandler levelCompleteHandler;
+    private LevelMouseClickHandler levelMouseClickHandler;
 
     public Game(DefenderMainView defenderMainView) {
 
         this.defenderMainView = defenderMainView;
+
+        this.player = new Player();
         this.level = new SimpleObjectProperty<Level>();
         this.difficulty = new SimpleObjectProperty<Difficulty>();
         this.paused = new SimpleBooleanProperty();
+        this.selectedTowerFactory = new SimpleObjectProperty<TowerFactory>();
 
         this.gameView = new GameView(this);
 
-        levelCompleteListener = new LevelCompleteListener();
+        levelCompleteHandler = new LevelCompleteHandler();
+        levelMouseClickHandler = new LevelMouseClickHandler();
+
         level.addListener(new ChangeListener<Level>() {
             @Override
             public void changed(ObservableValue<? extends Level> source, Level oldLevel, Level newLevel) {
                 if (oldLevel != null) {
-                    oldLevel.completeProperty().removeListener(levelCompleteListener);
+                    oldLevel.completeProperty().removeListener(levelCompleteHandler);
+                    oldLevel.setOnMouseClicked(null);
                 }
                 if (newLevel != null) {
                     log.debug("Adding level complete listener");
-                    newLevel.completeProperty().addListener(levelCompleteListener);
+                    newLevel.completeProperty().addListener(levelCompleteHandler);
+                    newLevel.setOnMouseClicked(levelMouseClickHandler);
+                }
+            }
+        });
+
+        player.livesProperty().addListener(new ChangeListener<Number>() {
+            @Override
+            public void changed(ObservableValue<? extends Number> source, Number oldLives, Number newLives) {
+                if (newLives.intValue() <= 0) {
+                    showGameLostView();
                 }
             }
         });
     }
 
-    public void showNewGameDialog() {
+    public Player getPlayer() {
+        return player;
+    }
+
+    public void showNewGameView() {
         setPaused(true);
         if (newGameView == null) {
             newGameView = new NewGameView(this);
         log.info("Starting new game with difficulty '{}'", difficulty);
         setDifficulty(difficulty);
         stopCurrentLevel();
-        nextLevel = 1;
+        nextLevel = 0;
         nextLevel();
     }
 
+
+    public void showLevelCompleteView(Level level) {
+        log.info("{} was completed!", level.getName());
+        if (levelCompleteView == null) {
+            levelCompleteView = new LevelCompleteView(Game.this);
+        }
+        levelCompleteView.setLevel(level);
+        defenderMainView.showView(levelCompleteView);
+    }
+
+    public void showGameWonView() {
+        setPaused(true);
+        if (gameWonView == null) {
+            gameWonView = new GameWonView(this);
+        }
+        defenderMainView.showView(gameWonView);
+    }
+
+    public void showGameLostView() {
+        setPaused(true);
+        if (gameLostView == null) {
+            gameLostView = new GameLostView(this);
+        }
+        defenderMainView.showView(gameLostView);
+    }
+
     public void nextLevel() {
         stopCurrentLevel();
-        Level nextLevel = createNextLevel();
+        Level nextLevel = loadNextLevel();
         log.info("Changing game level to: {}", nextLevel.getName());
         this.level.set(nextLevel);
-        nextLevel.start();
+        nextLevel.setActive(true);
         setPaused(false);
         defenderMainView.showView(gameView);
     }
-    
-    public Level getCurrentLevel() {
-        return currentLevel;
-    }
 
     public BooleanProperty pausedProperty() {
         return paused;
         this.difficulty.set(difficulty);
     }
 
+    public ObjectProperty<TowerFactory> selectedTowerFactoryProperty() {
+        return selectedTowerFactory;
+    }
+
+    public TowerFactory getSelectedTowerFactory() {
+        return selectedTowerFactory.get();
+    }
+
+    public void setSelectedTowerFactory(TowerFactory selectedTowerFactory) {
+        this.selectedTowerFactory.set(selectedTowerFactory);
+    }
+
     protected void stopCurrentLevel() {
         Level currentLevel = levelProperty().get();
         if (currentLevel != null) {
             log.debug("Stopping current game level: " + level.getName());
-            currentLevel.stop();
+            currentLevel.setActive(false);
         }
     }
 
-    protected Level createNextLevel() {
-        // todo can create weird and wonderful levels here - load from level file, etc
-//        return new SimpleLevel(this, "Level " + nextLevel++, 5);
-        return LevelFactory.getInstance().getLevel(this.getDifficulty(), this, nextLevel);
+    protected Level loadNextLevel() {
+        String nextLevelFile = LEVELS[nextLevel];
+        try {
+            FXMLLoader loader = new FXMLLoader();
+            InputStream nextLevelStream = getClass().getResourceAsStream(nextLevelFile);
+            Level level = (Level) loader.load(nextLevelStream);
+            level.setGame(this);
+            nextLevel++;
+            return level;
+        } catch (IOException e) {
+            throw new IllegalArgumentException("Unable to load next level from: '" + nextLevelFile + "'", e);
+        }
     }
 
     //---------------------------------------------------------------------------------------------
 
-    private final class LevelCompleteListener implements ChangeListener<Boolean> {
-
-        private LevelCompleteView levelCompleteView;
+    private final class LevelCompleteHandler implements ChangeListener<Boolean> {
 
         @Override
         public void changed(ObservableValue<? extends Boolean> source, Boolean wasComplete, Boolean isComplete) {
 
             if (isComplete) {
-                Level level = getLevel();
-                log.info("{} was completed!", level.getName());
-                if (levelCompleteView == null) {
-                    levelCompleteView = new LevelCompleteView(Game.this);
+                if (nextLevel < LEVELS.length) {
+                    showLevelCompleteView(getLevel());
+                } else {
+                    showGameWonView();
                 }
-                levelCompleteView.setLevel(level);
-                defenderMainView.showView(levelCompleteView);
+            }
+        }
+    }
+
+    //---------------------------------------------------------------------------------------------
+
+    private final class LevelMouseClickHandler implements EventHandler<MouseEvent> {
+
+        @Override
+        public void handle(MouseEvent event) {
+            Level currentLevel = level.get();
+            TowerFactory towerFactory = selectedTowerFactory.get();
+            if (currentLevel != null && towerFactory != null) {
+                log.info("Adding new tower '{}' at ({},{}) ",
+                        new Object[]{towerFactory.getTowerName(), event.getX(), event.getY()});
+                Tower tower = towerFactory.createTower();
+                if (currentLevel.canPlaceTowerAt(tower, event.getX(), event.getY())) {
+                    tower.setTranslateX(event.getX());
+                    tower.setTranslateY(event.getY());
+                    currentLevel.getTowers().add(tower);
+                }
             }
         }
     }

defender/src/main/java/com/fxexperience/games/defender/Player.java

+/*
+ * Copyright (c) 2012, FX Experience. 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 author 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 THE
+ * COPYRIGHT OWNER OR CONTRIBUTORS 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.
+ */
+package com.fxexperience.games.defender;
+
+import javafx.beans.property.IntegerProperty;
+import javafx.beans.property.SimpleIntegerProperty;
+
+public class Player {
+
+    private IntegerProperty credits;
+    private IntegerProperty lives;
+
+    public Player() {
+        credits = new SimpleIntegerProperty(200);
+        lives = new SimpleIntegerProperty(10);
+    }
+
+    public IntegerProperty creditsProperty() {
+        return credits;
+    }
+
+    public int getCredits() {
+        return credits.get();
+    }
+
+    public void setCredits(int credits) {
+        this.credits.set(credits);
+    }
+
+    public IntegerProperty livesProperty() {
+        return lives;
+    }
+
+    public int getLives() {
+        return lives.get();
+    }
+
+    public void setLives(int lives) {
+        this.lives.set(lives);
+    }
+
+    public void loseLife() {
+        setLives(getLives() - 1);
+    }
+
+    public void addCredits(int creditValue) {
+        setCredits(getCredits() + creditValue);
+    }
+}

defender/src/main/java/com/fxexperience/games/defender/Sprite.java

+/*
+ * Copyright (c) 2012, FX Experience. 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 author 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 THE
+ * COPYRIGHT OWNER OR CONTRIBUTORS 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.
+ */
+package com.fxexperience.games.defender;
+
+import com.fxexperience.games.defender.level.Level;
+import javafx.animation.Animation;
+import javafx.animation.AnimationTimer;
+import javafx.beans.property.BooleanProperty;
+import javafx.beans.property.SimpleBooleanProperty;
+import javafx.beans.value.ChangeListener;
+import javafx.beans.value.ObservableValue;
+import javafx.geometry.Bounds;
+import javafx.geometry.Point2D;
+import javafx.scene.Group;
+
+public class Sprite extends Group {
+
+    protected Level level;
+
+    private BooleanProperty active;
+    private Animation animation;
+    private AnimationTimer animationTimer;
+
+    public Sprite() {
+        this.active = new SimpleBooleanProperty();
+
+        this.active.addListener(new ChangeListener<Boolean>() {
+            @Override
+            public void changed(ObservableValue<? extends Boolean> source, Boolean wasActive, Boolean isActive) {
+                if (isActive) {
+                    if (animation != null) {
+                        animation.play();
+                    }
+                    if (animationTimer != null) {
+                        animationTimer.start();
+                    }
+                } else {
+                    if (animation != null) {
+                        animation.stop();
+                    }
+                    if (animationTimer != null) {
+                        animationTimer.stop();
+                    }
+                }
+            }
+        });
+    }
+
+    public Level getLevel() {
+        return level;
+    }
+
+    public void setLevel(Level level) {
+        this.level = level;
+    }
+
+    public BooleanProperty activeProperty() {
+        return this.active;
+    }
+
+    public boolean isActive() {
+        return active.get();
+    }
+
+    public void setActive(boolean active) {
+        this.active.set(active);
+    }
+
+    public void setAnimation(Animation animation) {
+        this.animation = animation;
+    }
+
+    public void setUpdateOnEveryFrame(boolean updateOnEveryFrame) {
+        // animation timer is a dodgy JFX class that makes clean architecture nasty - need to look at alternatives
+        // or talk to JFX team about cleaning this up
+        if (updateOnEveryFrame) {
+            if (animationTimer == null) {
+                animationTimer = new AnimationTimer() {
+                    @Override
+                    public void handle(long now) {
+                        update();
+                    }
+                };
+            }
+        } else {
+            if (animationTimer != null) {
+                animationTimer.stop();
+            }
+            animationTimer = null;
+        }
+    }
+
+    protected Point2D getMidPoint() {
+        Bounds bounds = getBoundsInParent();
+        double x = bounds.getMinX() + (bounds.getWidth() / 2);
+        double y = bounds.getMinY() + (bounds.getHeight() / 2);
+        return new Point2D(x,  y);
+    }
+
+    /**
+     * Can be overridden and used in conjunction with setUpdateOnEveryFrame(). This is a slightly dodgy way to do old
+     * school update on every frame style handling when an animation just doesn't cut it (e.g. when deciding whether or
+     * not to shoot at a BadGuy). There could be better ways to do this, but if not there should be. We should either
+     * find the better ways or talk to the JFX guys about making some better ways (e.g. using the Animation class with
+     * some sort of update callback would be more inline with the rest of the Animation API).
+     */
+    protected void update() {
+    }
+}

defender/src/main/java/com/fxexperience/games/defender/badguy/BadGuy.java

+/*
+ * Copyright (c) 2012, FX Experience. 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 author 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 THE
+ * COPYRIGHT OWNER OR CONTRIBUTORS 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.
+ */
+package com.fxexperience.games.defender.badguy;
+
+import com.fxexperience.games.defender.Sprite;
+import com.fxexperience.games.defender.level.Level;
+import javafx.animation.Interpolator;
+import javafx.animation.PathTransition;
+import javafx.beans.property.IntegerProperty;
+import javafx.beans.property.SimpleIntegerProperty;
+import javafx.beans.value.ChangeListener;
+import javafx.beans.value.ObservableValue;
+import javafx.event.ActionEvent;
+import javafx.event.EventHandler;
+import javafx.scene.shape.Path;
+import javafx.util.Duration;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+public class BadGuy extends Sprite {
+
+    private static final Logger log = LoggerFactory.getLogger(BadGuy.class);
+
+    private Duration initialDelay;
+    private Path path;
+    private IntegerProperty maxHitPoints;
+    private IntegerProperty hitPoints;
+    private IntegerProperty creditValue;
+
+    public BadGuy() {
+        maxHitPoints = new SimpleIntegerProperty(5);
+        hitPoints = new SimpleIntegerProperty(5);
+        creditValue = new SimpleIntegerProperty(20);
+
+        setTranslateX(-1000);
+        setTranslateY(-1000);
+
+        hitPoints.addListener(new ChangeListener<Number>() {
+            @Override
+            public void changed(ObservableValue<? extends Number> source, Number oldHitPoints, Number newHitPoints) {
+                log.debug("BadGuy took some damage, hit points now at {}", newHitPoints.intValue());
+                if (newHitPoints.intValue() <= 0) {
+                    die();
+                }
+            }
+        });
+    }
+
+    public void setMaxHitPoints(int maxHitPoints) {
+        this.maxHitPoints.set(maxHitPoints);
+        setHitPoints(maxHitPoints);
+    }
+
+    public int getMaxHitPoints() {
+        return maxHitPoints.get();
+    }
+
+    public IntegerProperty maxHitPoints() {
+        return maxHitPoints;
+    }
+
+    public void setHitPoints(int hitPoints) {
+        this.hitPoints.set(hitPoints);
+    }
+
+    public int getHitPoints() {
+        return hitPoints.get();
+    }
+
+    public IntegerProperty hitPoints() {
+        return hitPoints;
+    }
+
+    public void setCreditValue(int creditValue) {
+        this.creditValue.set(creditValue);
+    }
+
+    public int getCreditValue() {
+        return creditValue.get();
+    }
+
+    public IntegerProperty creditValue() {
+        return creditValue;
+    }
+
+    public Duration getInitialDelay() {
+        return initialDelay;
+    }
+
+    public void setInitialDelay(Duration initialDelay) {
+        this.initialDelay = initialDelay;
+    }
+
+    public Path getPath() {
+        return path;
+    }
+
+    public void setPath(Path path) {
+
+        this.path = path;
+
+        PathTransition pathTransition = new PathTransition(Duration.millis(10000), path, this);
+        if (initialDelay != null) {
+            pathTransition.setDelay(initialDelay);
+        }
+        pathTransition.setOrientation(PathTransition.OrientationType.ORTHOGONAL_TO_TANGENT);
+        pathTransition.setInterpolator(Interpolator.LINEAR);
+        pathTransition.setOnFinished(new EventHandler<ActionEvent>() {
+            @Override
+            public void handle(ActionEvent actionEvent) {
+                log.debug("BadGuy reached target destination");
+                level.getGame().getPlayer().loseLife();
+                level.getBadGuys().remove(BadGuy.this);
+            }
+        });
+        setAnimation(pathTransition);
+    }
+
+    protected void die() {
+
+        // todo play a cool death animation
+
+        Level level = getLevel();
+        if (level != null) {
+            level.getBadGuys().remove(this);
+            level.getGame().getPlayer().addCredits(getCreditValue());
+        }
+    }
+}

defender/src/main/java/com/fxexperience/games/defender/bullet/Bullet.java

+/*
+ * Copyright (c) 2012, FX Experience. 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 author 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 THE
+ * COPYRIGHT OWNER OR CONTRIBUTORS 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.
+ */
+package com.fxexperience.games.defender.bullet;
+
+import com.fxexperience.games.defender.Sprite;
+import com.fxexperience.games.defender.badguy.BadGuy;
+import javafx.animation.TranslateTransition;
+import javafx.event.ActionEvent;
+import javafx.event.EventHandler;
+import javafx.geometry.Point2D;
+import javafx.scene.paint.Color;
+import javafx.scene.shape.Circle;
+import javafx.util.Duration;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+public class Bullet extends Sprite {
+
+    private static final Logger log = LoggerFactory.getLogger(Bullet.class);
+
+    public Bullet(Point2D start, final BadGuy target, Duration timeToHit, final int damage) {
+
+        Circle body = new Circle(5);
+        body.setFill(Color.DARKRED);
+        getChildren().add(body);
+
+        setTranslateX(start.getX());
+        setTranslateY(start.getY());
+
+        TranslateTransition translateTransition = new TranslateTransition(timeToHit, this);
+        translateTransition.toXProperty().bind(target.translateXProperty());
+        translateTransition.toYProperty().bind(target.translateYProperty());
+        translateTransition.setOnFinished(new EventHandler<ActionEvent>() {
+            @Override
+            public void handle(ActionEvent actionEvent) {
+                log.debug("Bullet hit target for damage {}", damage);
+                level.getBullets().remove(Bullet.this);
+                target.hitPoints().set(target.getHitPoints() - damage);
+            }
+        });
+        setAnimation(translateTransition);
+    }
+}

defender/src/main/java/com/fxexperience/games/defender/level/Cell.java

-/*
- * Copyright (c) 2012, FX Experience. 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 author 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 THE
- * COPYRIGHT OWNER OR CONTRIBUTORS 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.
- */
-package com.fxexperience.games.defender.level;
-
-import com.fxexperience.games.defender.CONSTANTS;
-import com.fxexperience.games.defender.Game;
-import javafx.geometry.Point2D;
-import javafx.scene.layout.Pane;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
-
-/**
- *
- * @author Jose
- */
-public class Cell extends Pane {
-
-    ///////////////////////////////////////////////////////////////////////////
-    //variables
-    //...the mid point will be needed to put together the Path for the BadGuys
-    private Point2D midPoint;
-    private static final Logger log = LoggerFactory.getLogger(Cell.class);
-
-    ///////////////////////////////////////////////////////////////////////////
-    //constructors
-    public Cell(int col, int row) {
-        this.setWidth(CONSTANTS.CELL_DIMENSIONS.getWidth());
-        this.setHeight(CONSTANTS.CELL_DIMENSIONS.getHeight());
-        midPoint = new Point2D(col * this.getWidth() + this.getWidth() / 2, row * this.getHeight() + this.getHeight() / 2);
-//        log.debug("midPoint.getX() = " + midPoint.getX() + ", midPoint.getY() = " + midPoint.getY());
-    }
-    ///////////////////////////////////////////////////////////////////////////
-    //private functions
-    ///////////////////////////////////////////////////////////////////////////
-    //public functions    
-
-    public Point2D getMidPoint() {
-        return midPoint;
-    }
-}

defender/src/main/java/com/fxexperience/games/defender/level/EnemyPath.java

-/*
- * To change this template, choose Tools | Templates
- * and open the template in the editor.
- */
-package com.fxexperience.games.defender.level;
-
-import java.util.Arrays;
-import java.util.List;
-import java.util.ListIterator;
-import javafx.geometry.Point2D;
-import javafx.scene.shape.LineTo;
-import javafx.scene.shape.MoveTo;
-import javafx.scene.shape.Path;
-
-/**
- *
- * @author Jose
- */
-public class EnemyPath {
-
-    ///////////////////////////////////////////////////////////////////////////
-    //variables
-//    private List cells;
-    private Path path = null;
-    private String name = "";
-    private double pathDistance = 0;
-
-    ///////////////////////////////////////////////////////////////////////////
-    //constructors
-    public EnemyPath(Cell[] cellArray, String name) {
-        if (cellArray.length >= 2) {
-            List<Cell> cells = Arrays.asList(cellArray);
-            path = getPath(cells);
-            pathDistance = getPathDistance(cells);
-        } else {
-            throw new IllegalArgumentException("not enough cells");
-        }
-        if (name != null) {
-            this.name = name;
-        }
-    }
-
-    ///////////////////////////////////////////////////////////////////////////
-    //private
-    private Path getPath(List<Cell> cells) {
-        Path pth = new Path();
-        ListIterator<Cell> it = cells.listIterator();
-        while (it.hasNext()) {
-            Point2D pnt = it.next().getMidPoint();
-            if (it.previousIndex() == 0) {
-                pth.getElements().add(new MoveTo(pnt.getX(), pnt.getY()));
-            } else {
-                pth.getElements().add(new LineTo(pnt.getX(), pnt.getY()));
-            }
-        }
-        return pth;
-    }
-
-    private double getPathDistance(List<Cell> cells) {
-        Point2D p2 = null;
-        double distance = 0;
-        for (Cell cell : cells) {
-            Point2D p1 = cell.getMidPoint();
-            if (p2 != null) {
-                distance += Math.sqrt(Math.pow((p2.getX() - p1.getX()), 2) + Math.pow((p2.getY() - p1.getY()), 2));
-            }
-            p2 = p1;
-        }
-        return distance;
-    }
-
-    ///////////////////////////////////////////////////////////////////////////
-    //public functions
-    public Path getPath() {
-        return path;
-    }
-
-    public String getName() {
-        return name;
-    }
-
-    public double getPathDistance() {
-        return pathDistance;
-    }
-}

defender/src/main/java/com/fxexperience/games/defender/level/Grid.java

-/*
- * Copyright (c) 2012, FX Experience. 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 author 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 THE
- * COPYRIGHT OWNER OR CONTRIBUTORS 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.
- */
-package com.fxexperience.games.defender.level;
-
-import javafx.scene.Group;
-
-/**
- *
- * @author Jose
- */
-public class Grid extends Group {
-    ///////////////////////////////////////////////////////////////////////////
-    //private variables
-    
-    
-    ///////////////////////////////////////////////////////////////////////////
-    //constructors
-    ///////////////////////////////////////////////////////////////////////////
-    //private functions
-    ///////////////////////////////////////////////////////////////////////////
-    //public functions
-}

defender/src/main/java/com/fxexperience/games/defender/level/Level.java

 package com.fxexperience.games.defender.level;
 
 import com.fxexperience.games.defender.Game;
-import com.fxexperience.games.defender.sprite.BadGuy;
-import com.fxexperience.games.defender.sprite.Tower;
+import com.fxexperience.games.defender.Sprite;
+import com.fxexperience.games.defender.badguy.BadGuy;
+import com.fxexperience.games.defender.bullet.Bullet;
+import com.fxexperience.games.defender.tower.Tower;
+import javafx.beans.InvalidationListener;
+import javafx.beans.Observable;
 import javafx.beans.property.*;
 import javafx.collections.FXCollections;
+import javafx.collections.ListChangeListener;
 import javafx.collections.ObservableList;
+import javafx.geometry.Bounds;
 import javafx.scene.Group;
 import javafx.scene.shape.Rectangle;
+import javafx.scene.shape.Shape;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
-public abstract class Level extends Group {
+import java.util.ArrayList;
+import java.util.List;
+
+public class Level extends Group {
 
     private static final Logger log = LoggerFactory.getLogger(Level.class);
-    private final Game game;
-    private ReadOnlyStringProperty name;
+
+    private Game game;
+    private StringProperty name;
+    private BooleanProperty active;
     private BooleanProperty complete;
     private ReadOnlyObjectProperty<Rectangle> levelBounds;
     private ObservableList<BadGuy> badGuys;
     private ObservableList<Tower> towers;
+    private ObservableList<Bullet> bullets;
+    private ObservableList<Shape> allowedTowerAreas;
 
-    public Level(Game game, String name, double width, double height) {
-        this.game = game;
+    public Level() {
 
-        this.name = new ReadOnlyStringWrapper(name);
+        this.name = new SimpleStringProperty();
+        this.active = new SimpleBooleanProperty();
         this.complete = new SimpleBooleanProperty();
         this.badGuys = FXCollections.observableArrayList();
         this.towers = FXCollections.observableArrayList();
+        this.bullets = FXCollections.observableArrayList();
+        this.allowedTowerAreas = FXCollections.observableArrayList();
 
-        Rectangle bounds = new Rectangle(0, 0, width, height);
+        Rectangle bounds = new Rectangle(0, 0, 800, 600);
         setClip(bounds);
         this.levelBounds = new ReadOnlyObjectWrapper<Rectangle>(bounds);
+
+        // show allowed tower areas on the map (useful for testing - normally the background would indicate this)
+
+        allowedTowerAreas.addListener(new ListChangeListener<Shape>() {
+            @Override
+            public void onChanged(Change<? extends Shape> change) {
+                while (change.next()) {
+                    if (change.wasAdded()) {
+                        List<? extends Shape> addedSubList = change.getAddedSubList();
+                        getChildren().addAll(addedSubList);
+                    } else if (change.wasRemoved()) {
+                        List<? extends Shape> removed = change.getRemoved();
+                        getChildren().removeAll(removed);
+                    } else {
+                        throw new IllegalArgumentException("This type of change is not handled");
+                    }
+                }
+            }
+        });
+
+
+        // listen for sprites being added and removed and hook them up accordingly
+
+        ListChangeListener<Sprite> spriteListChangeListener = new ListChangeListener<Sprite>() {
+            @Override
+            public void onChanged(Change<? extends Sprite> change) {
+
+                while (change.next()) {
+                    if (change.wasAdded()) {
+                        List<? extends Sprite> addedSubList = change.getAddedSubList();
+                        for (Sprite sprite : addedSubList) {
+                            sprite.setLevel(Level.this);
+                            sprite.activeProperty().bind(activeProperty());
+                        }
+                        getChildren().addAll(addedSubList);
+                    } else if (change.wasRemoved()) {
+                        List<? extends Sprite> removed = change.getRemoved();
+                        for (Sprite sprite : removed) {
+                            sprite.activeProperty().unbind();
+                            sprite.setActive(false);
+                            sprite.setLevel(null);
+                        }
+                        getChildren().removeAll(removed);
+                    } else {
+                        throw new IllegalArgumentException("This type of change is not handled");
+                    }
+                }
+            }
+        };
+
+        badGuys.addListener((ListChangeListener) spriteListChangeListener);
+        towers.addListener((ListChangeListener) spriteListChangeListener);
+        bullets.addListener((ListChangeListener) spriteListChangeListener);
+
+        // watch for when the Level is complete
+
+        badGuys.addListener(new InvalidationListener() {
+            @Override
+            public void invalidated(Observable source) {
+                setComplete(badGuys.size() == 0);
+            }
+        });
     }
 
     public Game getGame() {
         return game;
     }
 
-    public ReadOnlyStringProperty nameProperty() {
+    public void setGame(Game game) {
+        this.game = game;
+    }
+
+    public StringProperty nameProperty() {
         return name;
     }
 
         return name.get();
     }
 
+    public void setName(String name) {
+        this.name.set(name);
+    }
+
+    public void setWidth(int width) {
+        levelBounds.get().setWidth(width);
+    }
+
+    public void setHeight(int height) {
+        levelBounds.get().setHeight(height);
+    }
+
     public ReadOnlyObjectProperty<Rectangle> levelBounds() {
         return levelBounds;
     }
         return levelBounds.get();
     }
 
-    public void start() {
-    }
-
-    public void stop() {
-    }
-
     public BooleanProperty completeProperty() {
         return this.complete;
     }
         this.complete.set(complete);
     }
 
-    public void addBadGuy(BadGuy badGuy) {
-        badGuy.start(this);
-        badGuys.add(badGuy);
-        // probably could do some clever binding instead of this...
-        getChildren().add(badGuy);
-        log.debug("BadGuy added to level, we now have {} BadGuys on the Level", badGuys.size());
+
+    public BooleanProperty activeProperty() {
+        return this.active;
+    }
+
+    public boolean isActive() {
+        return active.get();
+    }
+
+    public void setActive(boolean active) {
+        this.active.set(active);
     }
 
     public ObservableList<BadGuy> getBadGuys() {
         return badGuys;
     }
 
-    public void removeBadGuy(BadGuy badGuy) {
-        badGuy.stop();
-        badGuys.remove(badGuy);
-        // probably could do some clever binding instead of this...
-        getChildren().remove(badGuy);
-        log.debug("BadGuy removed from level, we now have {} BadGuys on the Level", badGuys.size());
+    public ObservableList<Tower> getTowers() {
+        return towers;
+    }
+
+    public ObservableList<Bullet> getBullets() {
+        return bullets;
+    }
+
+    public ObservableList<Shape> getAllowedTowerAreas() {
+        return allowedTowerAreas;
+    }
+
+    public List<BadGuy> findBadGuysInArea(Shape area) {
+
+        List<BadGuy> matches = new ArrayList<>();
+        for (BadGuy badGuy : badGuys) {
+            // shape intersection isn't really all that in JFX - need to do something smarter here
+            if (badGuy.getBoundsInParent().intersects(area.getBoundsInParent())) {
+                matches.add(badGuy);
+            }
+        }
+        return matches;
+    }
+
+    public boolean canPlaceTowerAt(Tower tower, double x, double y)  {
+
+        // this is a very crude algorithm - need to do proper shape intersection
+        for (Shape allowedTowerArea : allowedTowerAreas) {
+            if (allowedTowerArea.contains(x, y)) {
+                Bounds box = allowedTowerArea.getBoundsInParent();
+                double towerRadius = tower.getRadius();
+                if (box.getMinX() < x - towerRadius && box.getMaxX() > x + towerRadius
+                        && box.getMinY() < y - towerRadius && box.getMaxY() > y + towerRadius) {
+                    return true;
+                }
+            }
+        }
+        return false;
     }
 }

defender/src/main/java/com/fxexperience/games/defender/level/LevelFactory.java

-/*
- * To change this template, choose Tools | Templates
- * and open the template in the editor.
- */
-package com.fxexperience.games.defender.level;
-
-import com.fxexperience.games.defender.CONSTANTS;
-import com.fxexperience.games.defender.Difficulty;
-import com.fxexperience.games.defender.Game;
-import java.util.LinkedList;
-
-/**
- *
- * @author Jose
- */
-public class LevelFactory {
-
-    ///////////////////////////////////////////////////////////////////////////
-    //variables
-    private static LevelFactory factory = null;
-
-    ///////////////////////////////////////////////////////////////////////////
-    //constructors
-    public static LevelFactory getInstance() {
-        if (factory == null) {
-            factory = new LevelFactory();
-        }
-        return factory;
-    }
-
-    private LevelFactory() {
-    }
-
-    ///////////////////////////////////////////////////////////////////////////
-    //private functions
-    ///////////////////////////////////////////////////////////////////////////
-    //public functions
-    public Level getLevel(Difficulty d, Game game, int levNum) {
-
-        LinkedList<WaveMaker> waveMakers = new LinkedList<>();
-
-        Cell start1 = new Cell(-1, 0);
-        Cell start2 = new Cell(-1, 15);
-        Cell start3 = new Cell(-1, 30);
-
-        Cell mid = new Cell(20, 15);
-
-        Cell end1 = new Cell(41, 0);
-        Cell end2 = new Cell(41, 15);
-        Cell end3 = new Cell(41, 30);
-
-        Cell[] path1 = {start1, mid, end1};
-        Cell[] path2 = {start2, mid, end1};
-        Cell[] path3 = {start3, mid, end1};
-        Cell[] path4 = {start1, mid, end3};
-        Cell[] path5 = {start3, mid, end2};
-
-        EnemyPath ep1 = new EnemyPath(CONSTANTS.CELLS_FOR_TEST, "one");
-        EnemyPath ep2 = new EnemyPath(path2, "two");
-        EnemyPath ep3 = new EnemyPath(path3, "three");
-        EnemyPath ep4 = new EnemyPath(path4, "four");
-        EnemyPath ep5 = new EnemyPath(path5, "five");
-
-        WaveMaker wm1 = new WaveMaker(50);
-        wm1.addEnemies(5, "blue", ep1);
-        wm1.addEnemies(5, "green", ep2);
-
-        WaveMaker wm2 = new WaveMaker(50);
-        wm2.addEnemies(5, "red", ep3);
-        wm2.addEnemies(5, "blue", ep5);
-        wm2.addEnemies(5, "blue", ep4);
-
-        WaveMaker wm3 = new WaveMaker(50);
-        wm3.addEnemies(5, "red", ep1);
-        wm3.addEnemies(5, "blue", ep5);
-        wm3.addEnemies(5, "blue", ep2);
-
-        waveMakers.clear();
-        waveMakers.add(wm1);
-        waveMakers.add(wm2);
-        waveMakers.add(wm3);
-        WaveLevel wl = new WaveLevel(game, "Wave Level" + levNum);
-        wl.setWaveMakers(waveMakers);
-        return wl;
-    }
-}

defender/src/main/java/com/fxexperience/games/defender/level/SimpleLevel.java

-/*
- * Copyright (c) 2012, FX Experience. 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 author 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 THE
- * COPYRIGHT OWNER OR CONTRIBUTORS 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.
- */
-package com.fxexperience.games.defender.level;
-
-import com.fxexperience.games.defender.CONSTANTS;
-import com.fxexperience.games.defender.Game;
-import com.fxexperience.games.defender.sprite.BadGuy;
-import com.fxexperience.games.defender.sprite.BadGuyFactory;
-import javafx.animation.KeyFrame;
-import javafx.animation.Timeline;
-import javafx.beans.InvalidationListener;
-import javafx.beans.Observable;
-import javafx.beans.value.ChangeListener;
-import javafx.beans.value.ObservableValue;
-import javafx.collections.ObservableList;
-import javafx.event.ActionEvent;
-import javafx.event.EventHandler;
-import javafx.geometry.Point2D;
-import javafx.scene.paint.Color;
-import javafx.scene.shape.Rectangle;
-import javafx.util.Duration;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
-
-/**
- * Very simple level just for testing the basics. More interesting levels can be
- * added later.
- */
-public class SimpleLevel extends Level {
-
-    private static final Logger log = LoggerFactory.getLogger(SimpleLevel.class);
-    private int numBadGuys;
-    private Timeline timeline;
-    //...needed for EventHandler
-    private SimpleLevel me;
-
-    public SimpleLevel(Game game, String name, int numBadGuys) {
-
-        super(game, name, CONSTANTS.LEVEL_WIDTH, CONSTANTS.LEVEL_HEIGHT);
-        this.numBadGuys = numBadGuys;
-        getStyleClass().add("simpleLevel");
-
-        // background
-
-        Rectangle levelBounds = getLevelBounds();
-        Rectangle background = new Rectangle();
-        background.widthProperty().bind(levelBounds.widthProperty());
-        background.heightProperty().bind(levelBounds.heightProperty());
-        background.setFill(Color.GHOSTWHITE);
-        background.setStroke(Color.DARKBLUE);
-        getChildren().add(background);
-
-        me = this;
-    }
-
-    @Override
-    public void start() {
-
-        // bad guy generation
-
-        timeline = new Timeline();
-        timeline.setCycleCount(numBadGuys);
-        KeyFrame addBadGuy = new KeyFrame(Duration.millis(2000), new EventHandler<ActionEvent>() {
-            @Override
-            public void handle(ActionEvent actionEvent) {
-//                BadGuy bg = new BadGuy(me, getGame(), new Point2D(-20, 40), new Point2D(800, 600), 100);//...pixels/sec for speed
-                BadGuy bg = BadGuyFactory.getInstance().getBadGuy(me, getGame(), "blue");
-                bg.setEnemyPath(new EnemyPath(CONSTANTS.CELLS_FOR_TEST, "testPath"), 120);//...pixels/sec for speed
-                addBadGuy(bg);
-            }
-        });
-        timeline.getKeyFrames().add(addBadGuy);
-        timeline.playFromStart();
-
-        // handle pauses
-
-        getGame().pausedProperty().addListener(new ChangeListener<Boolean>() {
-            @Override
-            public void changed(ObservableValue<? extends Boolean> source, Boolean wasPaused, Boolean isPaused) {
-                if (isPaused) {
-                    timeline.pause();
-                } else {
-                    if (!isComplete()) {
-                        timeline.play();
-                    }
-                }
-            }
-        });
-
-        // detect when the level is finished
-
-        timeline.setOnFinished(new EventHandler<ActionEvent>() {
-            @Override
-            public void handle(ActionEvent actionEvent) {
-                // finished making BadGuys, now wait for the last of the guys on the screen to go
-
-                final ObservableList<BadGuy> badGuys = getBadGuys();
-                if (badGuys.size() == 0) {
-                    // already done
-                    setComplete(true);
-                } else {
-                    // wait till list is empty
-                    badGuys.addListener(new InvalidationListener() {
-                        @Override
-                        public void invalidated(Observable source) {
-                            if (badGuys.size() == 0) {
-                                badGuys.removeListener(this);
-                                setComplete(true);
-                            }
-                        }
-                    });
-                }
-            }
-        });
-    }
-
-    @Override
-    public void stop() {
-        timeline.stop();
-    }
-}

defender/src/main/java/com/fxexperience/games/defender/level/Wave.java

-/*
- * To change this template, choose Tools | Templates
- * and open the template in the editor.
- */
-package com.fxexperience.games.defender.level;
-
-import java.util.LinkedList;
-import javafx.animation.KeyFrame;
-import javafx.animation.Timeline;
-import javafx.event.ActionEvent;
-import javafx.event.EventHandler;
-import javafx.util.Duration;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
-
-/**
- *
- * @author Jose
- */
-public class Wave {
-
-    ///////////////////////////////////////////////////////////////////////////
-    //private variables
-    private static final Logger log = LoggerFactory.getLogger(Wave.class);
-    
-    private Timeline slotExecutorTimeline = new Timeline();
-    private LinkedList<WaveSlot> slots = new LinkedList<>();
-    private Duration cummulativeSlotDelay = Duration.ONE;
-    private Level level;
-    private EventHandler<ActionEvent> runNextSlot = new EventHandler<ActionEvent>() {
-        @Override
-        public void handle(ActionEvent arg0) {
-            slots.remove().run(level);
-        }
-    };
-
-    ///////////////////////////////////////////////////////////////////////////
-    //constructors
-    public Wave(Level level) {
-        if (level != null) {
-            this.level = level;
-        } else {
-            throw new IllegalArgumentException("level is null");
-        }
-    }
-
-    ///////////////////////////////////////////////////////////////////////////
-    //public functions
-    public void run() {
-        slotExecutorTimeline.playFromStart();
-    }
-
-    public Timeline getTimeline() {
-        return slotExecutorTimeline;
-    }
-
-    public int getEnemyUnitCount() {
-        int count = 0;
-        for (WaveSlot slot : slots) {
-            count += slot.getBadGuyCount();
-        }
-        return count;
-    }
-
-    public void addNextWaveSlot(WaveSlot slot) {
-        log.debug("in Wave.addNextWaveSlot");
-        slotExecutorTimeline.getKeyFrames().add(new KeyFrame(cummulativeSlotDelay, runNextSlot));
-        cummulativeSlotDelay = cummulativeSlotDelay.add(slot.getNextSlotDelay());
-        slots.add(slot);
-    }
-}

defender/src/main/java/com/fxexperience/games/defender/level/WaveLevel.java

-/*
- * To change this template, choose Tools | Templates
- * and open the template in the editor.
- */
-package com.fxexperience.games.defender.level;
-
-import com.fxexperience.games.defender.CONSTANTS;
-import com.fxexperience.games.defender.Game;
-import com.fxexperience.games.defender.sprite.BadGuy;
-import java.util.LinkedList;
-import java.util.List;
-import javafx.animation.Timeline;
-import javafx.beans.value.ChangeListener;
-import javafx.beans.value.ObservableValue;
-import javafx.scene.paint.Color;
-import javafx.scene.shape.Rectangle;
-
-/**
- *
- * @author Jose
- */
-public class WaveLevel extends Level {
-
-    ///////////////////////////////////////////////////////////////////////////
-    //variables
-    private int unitsRemainingInRound = 0;
-    private int unitsRemainingInWave = 0;
-    private float roundSpeed = 1;
-    private float roundHpMultiplier = 1;
-    private LinkedList<Wave> activeWaves = new LinkedList<>();
-    private List<WaveMaker> waveMakers = null;
-    protected Timeline runWavesTL;
-    //...needed for EventHandler
-    private WaveLevel me;
-
-    ///////////////////////////////////////////////////////////////////////////
-    //constructors
-    public WaveLevel(Game game, String name) {
-
-        super(game, name, CONSTANTS.LEVEL_WIDTH, CONSTANTS.LEVEL_HEIGHT);
-//        this.numBadGuys = numBadGuys;
-        getStyleClass().add("simpleLevel");
-
-        // background
-
-        Rectangle levelBounds = getLevelBounds();
-        Rectangle background = new Rectangle();
-        background.widthProperty().bind(levelBounds.widthProperty());
-        background.heightProperty().bind(levelBounds.heightProperty());
-        background.setFill(Color.GHOSTWHITE);
-        background.setStroke(Color.DARKBLUE);
-        getChildren().add(background);
-
-        me = this;
-    }
-
-    ///////////////////////////////////////////////////////////////////////////
-    //private functions
-    private void makeWaves() {
-        activeWaves.clear();
-        for (WaveMaker wm : waveMakers) {
-//            activeWaves.add(wm.getWave(roundSpeed * CONSTANTS.ENEMY_UNIT_BASE_SPEED, roundHpMultiplier));
-            activeWaves.add(wm.getWave(roundSpeed * CONSTANTS.ENEMY_UNIT_BASE_SPEED, this, getGame()));
-        }
-    }
-
-    private void runNextWave() {
-        if (unitsRemainingInWave != 0) {
-            return;
-        }
-
-        Wave wave = activeWaves.remove();
-        unitsRemainingInWave = wave.getEnemyUnitCount();
-        runWavesTL = wave.getTimeline();
-        wave.run();
-    }
-
-    private Timeline getWavesTL() {
-        return this.runWavesTL;
-    }
-
-    ///////////////////////////////////////////////////////////////////////////
-    //public functions
-    public void setWaveMakers(List<WaveMaker> waveMakers) {
-        this.waveMakers = waveMakers;
-    }
-
-    @Override
-    public void start() {
-        // bad guy generation
-
-//        timeline = new Timeline();
-//        timeline.setCycleCount(numBadGuys);
-//        KeyFrame addBadGuy = new KeyFrame(Duration.millis(2000), new EventHandler<ActionEvent>() {
-//            @Override
-//            public void handle(ActionEvent actionEvent) {
-//                addBadGuy(new BadGuy(me, getGame(), new Point2D(-20, 40), new Point2D(800, 600), Math.max(0.3, Math.random())));
-//            }
-//        });
-//        timeline.getKeyFrames().add(addBadGuy);
-//        timeline.playFromStart();
-
-        unitsRemainingInRound = 0;
-        makeWaves();
-        for (Wave wave : activeWaves) {
-            unitsRemainingInRound = unitsRemainingInRound + wave.getEnemyUnitCount();
-        }
-
-        // handle pauses
-        getGame().pausedProperty().addListener(new ChangeListener<Boolean>() {
-            @Override
-            public void changed(ObservableValue<? extends Boolean> source, Boolean wasPaused, Boolean isPaused) {
-                if (isPaused) {
-                    getWavesTL().pause();
-                } else {
-                    if (!isComplete()) {
-                        getWavesTL().play();
-                    }
-                }
-            }
-        });
-
-        runNextWave();
-    }
-
-    @Override
-    public void stop() {
-        if (runWavesTL != null) {
-            runWavesTL.stop();
-        }
-    }
-
-    @Override
-    public void removeBadGuy(BadGuy badGuy) {
-        super.removeBadGuy(badGuy);
-
-        unitsRemainingInRound--;
-        unitsRemainingInWave--;
-        if (unitsRemainingInRound > 0) {
-            if (unitsRemainingInWave <= 0) {
-                runNextWave();
-            }
-        } else {
-            setComplete(true);
-        }
-
-    }
-}

defender/src/main/java/com/fxexperience/games/defender/level/WaveMaker.java

-/*
- * To change this template, choose Tools | Templates
- * and open the template in the editor.
- */
-package com.fxexperience.games.defender.level;
-
-import com.fxexperience.games.defender.Game;
-import com.fxexperience.games.defender.sprite.BadGuy;
-import com.fxexperience.games.defender.sprite.BadGuyFactory;
-import java.util.LinkedList;
-import java.util.List;
-import java.util.concurrent.ThreadLocalRandom;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
-
-/**
- *
- * @author Jose
- */
-public class WaveMaker {
-
-    ///////////////////////////////////////////////////////////////////////
-    //private static variables
-    //
-    ///////////////////////////////////////////////////////////////////////////
-    //private variables
-    private int unitDensity;
-    private List<BadGuyHelper> badGuyHelpers = new LinkedList<>();
-    private List<EnemyPathHelper> enemyPathHelpers = new LinkedList<>();
-    private Wave bigWave;
-    private static final Logger log = LoggerFactory.getLogger(WaveMaker.class);    
-
-    ///////////////////////////////////////////////////////////////////////////
-    //constructor
-    public WaveMaker(int unitDensity) {
-        assert 0 < unitDensity && 100 >= unitDensity;
-        this.unitDensity = unitDensity;
-    }
-
-    ///////////////////////////////////////////////////////////////////////////
-    //public functions
-    //
-    //...addEnemy adds a BadGuy to the WaveMaker.
-    public void addEnemies(int count, String enemyName, EnemyPath... ePaths) {
-        if (ePaths == null) {
-            return;
-        }
-        BadGuyHelper enemyHelper = new BadGuyHelper(enemyName, count);
-        badGuyHelpers.add(enemyHelper);
-        for (EnemyPath ep : ePaths) {
-            boolean found = false;
-            //...check if mp is already in a MapPathHelper
-            for (EnemyPathHelper eph : enemyPathHelpers) {
-                assert eph != null;
-                assert ep != null;
-                assert eph.enemyPath != null;
-                if (eph.enemyPath.getName().equals(ep.getName())) {
-                    found = true;
-                    //...enemyPathHelper found, add enemy to it
-                    eph.addBadGuyHelper(enemyHelper);
-                    break;
-                }
-            }
-            if (found == false) {//...enemyPathHelper not found, create a new one and add enemy to it
-                EnemyPathHelper mph = new EnemyPathHelper(ep);
-                mph.addBadGuyHelper(enemyHelper);
-                enemyPathHelpers.add(mph);
-            }
-        }
-    }
-
-//    public Wave getWave(float speed, float hpMultiplier) {
-    public Wave getWave(float speed, Level level, Game game) {
-        makeWave(speed, level, game);
-        return bigWave;
-    }
-
-    ///////////////////////////////////////////////////////////////////////////
-    //private functions
-//    private void makeWave(float speed, float hpMultiplier) {
-    private void makeWave(float speed, Level level, Game game) {
-        bigWave = new Wave(level);
-        int unitsCreated = 0;
-        int unitsRequired = 0;
-        for (BadGuyHelper eH : badGuyHelpers) {
-            eH.reset();
-            unitsRequired += eH.remainingCount;
-        }
-        while (unitsCreated < unitsRequired) {
-            WaveSlot slot = new WaveSlot();
-            for (EnemyPathHelper eph : enemyPathHelpers) {
-                if (eph.isClosed()) {
-                    eph.decrementCloseCount();
-                    continue;
-                }
-                if (eph.getRemaining() <= 0) {
-                    continue;
-                }
-                if (ThreadLocalRandom.current().nextInt(100) >= unitDensity) {//...if true, continue
-                    continue;
-                }//... if false, make enemy and add it to wave
-                //<make badGuy and add to wave>
-                String enemyName = eph.getRandomBadGuy();
-                if (enemyName == null) {
-                    log.debug("in WaveMaker.makeWaves, enemyName is null");
-                    continue;
-                }
-                BadGuy badGuy = BadGuyFactory.getInstance().getBadGuy(level, game, enemyName);
-                if (badGuy == null) {
-                    log.debug("in WaveMaker.makeWaves, enemy is null");
-                    continue;
-                }
-                badGuy.setEnemyPath(eph.enemyPath, speed);
-                eph.setCloseCount(badGuy.getUnitSize() - 1);
-                slot.addEnemyUnit(badGuy);
-                unitsCreated++;
-                //</>
-                if (unitsCreated >= unitsRequired) {