+ * Redistribution and use in source and binary forms, with or without modification, are permitted provided that the
+ * following conditions are met: 1. Redistributions of source code must retain the above copyright notice, this list
+ * of conditions and the following disclaimer. 2. 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. 3. Neither the name of the copyright holders 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 ''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 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.
+ *
+ *
+ * License specifications from the Adafruit C++ implementation:
+ *
+ *
+ * Author: K. Townsend (Adafruit Industries)
+ *
+ *
+ * LICENSE
+ *
+ *
+ * Software License Agreement (BSD License)
+ *
+ *
+ * Copyright (c) 2013, Adafruit Industries All rights reserved.
+ *
+ *
+ * Redistribution and use in source and binary forms, with or without modification, are permitted provided that the
+ * following conditions are met: 1. Redistributions of source code must retain the above copyright notice, this list
+ * of conditions and the following disclaimer. 2. 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. 3. Neither the name of the copyright holders 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 ''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 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.
+ *
+ */
+public class Tsl2561 {
+ /**
+ * Helper class to store the measured raw brightness values and calculate SI lux from them
+ */
+ static class Luminosity {
+ private final static int LUX_SCALE = 14; // Scale by 2^14
+ private final static int RATIO_SCALE = 9; // Scale ratio by 2^9
+ private final static int CHSCALE = 10; // Scale channel values by 2^10
+ private final static int INITIAL_SCALE_13MS = 0x7517; // 322/11 *2^CHSCALE
+ private final static int INITIAL_SCALE_101MS = 0x0FE7; // 322/81 *2^CHSCALE
+ private final static int INITIAL_SCALE_402MS = (1 << CHSCALE);
+
+ /**
+ * To calculate lux values from the measured broadband and infrared values, one has to do some computation. They rely
+ * on several factors that were obtained experimentally. Which factors has to be used depend on the ratio of the
+ * measured broadband and infrared values
+ *
+ * The factors used here apply only to the CL, FN and T package. If you use the CS package, please look into the
+ * TSL2561 documentation for the factors you have to use
+ *
+ */
+ private enum Factors {
+ LEVEL_1(0x0040, 0x01f2, 0x01be), // 0.125 * 2^RATIO_SCALE, 0.0304 * 2^LUX_SCALE, 0.0272 * 2^LUX_SCALE
+ LEVEL_2(0x0080, 0x0214, 0x02d1), // 0.250 * 2^RATIO_SCALE, 0.0325 * 2^LUX_SCALE, 0.0440 * 2^LUX_SCALE
+ LEVEL_3(0x00c0, 0x023f, 0x037b), // 0.375 * 2^RATIO_SCALE, 0.0351 * 2^LUX_SCALE, 0.0544 * 2^LUX_SCALE
+ LEVEL_4(0x0100, 0x0270, 0x03fe), // 0.50 * 2^RATIO_SCALE, 0.0381 * 2^LUX_SCALE, 0.0624 * 2^LUX_SCALE
+ LEVEL_5(0x0138, 0x016f, 0x01fc), // 0.61 * 2^RATIO_SCALE, 0.0224 * 2^LUX_SCALE, 0.0310 * 2^LUX_SCALE
+ LEVEL_6(0x019a, 0x00d2, 0x00fb), // 0.80 * 2^RATIO_SCALE, 0.0128 * 2^LUX_SCALE, 0.0153 * 2^LUX_SCALE
+ LEVEL_7(0x029a, 0x0018, 0x0012); // 1.3 * 2^RATIO_SCALE, 0.00146 * 2^LUX_SCALE, 0.00112 * 2^LUX_SCALE
+
+ /**
+ * Threshold of the ratio between broadband value and infrared value up to which the level is used
+ */
+ private final int mRatioThreshold;
+ /**
+ * ratio-dependent factor for the broadband value
+ */
+ private final int mBroadbandFactor;
+ /**
+ * ration-dependent factor for the infrared value
+ */
+ private final int mInfraredFactor;
+
+ Factors(final int aRatioThreshold, final int aBroadbandFactor, final int aInfraredFactor) {
+ mRatioThreshold = aRatioThreshold;
+ mBroadbandFactor = aBroadbandFactor;
+ mInfraredFactor = aInfraredFactor;
+ }
+
+ public int getBroadbandFactor() {
+ return mBroadbandFactor;
+ }
+
+ public int getInfraredFactor() {
+ return mInfraredFactor;
+ }
+
+ /**
+ * Return the correct factors for the given ratio
+ *
+ * @param aRatio
+ * the ratio between the measured broadband and infrared values
+ * @return the entry matching the specified ratio
+ */
+ public static Factors getFactorsByRatio(final long aRatio) {
+ for (final Factors factors : values()) {
+ if (aRatio < factors.mRatioThreshold) {
+ return factors;
+ }
+ }
+
+ return null;
+ }
+ }
+
+ final int mBroadband;
+ final int mInfrared;
+ final IntegrationTime mIntegrationTime;
+ final Gain mGain;
+
+ /**
+ * Constructor, the measured raw brightness values are given as byte arrays in little endian order
+ *
+ * @param aBroadband
+ * @param aInfrared
+ * @param mGain
+ * @param mIntegrationTime
+ */
+ public Luminosity(final byte[] aBroadband, final byte[] aInfrared, final IntegrationTime aIntegrationTime,
+ final Gain aGain) {
+ final ByteBuffer broadbandBuffer = ByteBuffer.wrap(aBroadband);
+ broadbandBuffer.order(ByteOrder.LITTLE_ENDIAN);
+ mBroadband = broadbandBuffer.getShort() & 0xFFFF;
+
+ final ByteBuffer infraredBuffer = ByteBuffer.wrap(aInfrared);
+ infraredBuffer.order(ByteOrder.LITTLE_ENDIAN);
+ mInfrared = infraredBuffer.getShort() & 0xFFFF;
+
+ mIntegrationTime = aIntegrationTime;
+ mGain = aGain;
+ }
+
+ public String toString() {
+ return "Luminosity broadband=" + mBroadband + ", infrared=" + mInfrared + ", gain=" + mGain + ", time="
+ + mIntegrationTime;
+ }
+
+ public int getBroadband() {
+ return mBroadband;
+ }
+
+ public int getInfrared() {
+ return mInfrared;
+ }
+
+ /**
+ * Converts the raw sensor values to the standard SI lux equivalent.
+ *
+ * @return {@link Long#MAX_VALUE} if the sensor is saturated such that the values are unreliable, 0 if at least one
+ * of the measured raw values is zero and the calculated lux value otherwise
+ */
+ public double calculateLux() {
+ long channelScale;
+
+ if (getBroadband() > mIntegrationTime.getClipping() || getInfrared() > mIntegrationTime.getClipping()) {
+ return Long.MAX_VALUE;
+ }
+ else if (getBroadband() == 0 || getInfrared() == 0) {
+ return 0;
+ }
+
+ // Initialize scale depending on the integration time
+ switch (mIntegrationTime) {
+ case MS_13:
+ channelScale = INITIAL_SCALE_13MS;
+ break;
+ case MS_101:
+ channelScale = INITIAL_SCALE_101MS;
+ break;
+ case MS_402:
+ channelScale = INITIAL_SCALE_402MS;
+ break;
+ default:
+ throw new IllegalArgumentException("Integration time " + mIntegrationTime + " cannot be handled");
+ }
+
+ // Modify scale based on the gain
+ if (mGain == Gain.X1) {
+ channelScale = channelScale << 4;
+ }
+
+ final long scaledBroadband = (getBroadband() * channelScale) >> CHSCALE;
+ final long scaledInfrared = (getInfrared() * channelScale) >> CHSCALE;
+
+ final long ratio;
+ if (scaledBroadband == 0) {
+ ratio = 0;
+ }
+ else {
+ // round during bit shifting
+ ratio = (((scaledInfrared << (RATIO_SCALE + 1)) / scaledBroadband) + 1) >> 1;
+ }
+
+ long temp = 0;
+
+ if (ratio > 0) {
+ final Factors factors = Factors.getFactorsByRatio(ratio);
+ if (factors != null) {
+ temp = ((scaledBroadband * factors.getBroadbandFactor()) - (scaledInfrared * factors.getInfraredFactor()));
+ }
+ }
+
+ final double lux;
+ if (temp >= 0) {
+ lux = (temp >> (LUX_SCALE - 1)) / 10.0;
+ }
+ else {
+ lux = 0;
+ }
+
+ return lux;
+ }
+ }
+
+ /**
+ * Codes to switch the sensor on or off
+ */
+ enum Power {
+ ON(0x03), //
+ OFF(0x00);
+
+ private int mFieldValue;
+
+ Power(final int aFieldValue) {
+ mFieldValue = aFieldValue;
+ }
+
+ public int getFieldValue() {
+ return mFieldValue;
+ }
+ }
+
+ /**
+ * Registers of the light sensor that are used
+ */
+ enum Register {
+ CONTROL(0x00), //
+ TIMING(0x01), //
+ BROADBAND(0x0C), //
+ INFRARED(0x0E);
+
+ private int mRegister;
+
+ Register(final int aRegister) {
+ mRegister = aRegister;
+ }
+
+ public int getRegister() {
+ return mRegister;
+ }
+ }
+
+ /**
+ * Integration time options that are supported by the sensor
+ */
+ enum IntegrationTime {
+ MS_13(0x00, 15, 100, 4850, 4900), //
+ MS_101(0x01, 120, 200, 36000, 37000), //
+ MS_402(0x02, 450, 500, 63000, 65000);
+
+ /**
+ * Value to write to the timing register to set the integration time
+ */
+ private int mFieldValue;
+ /**
+ * Integration time in milliseconds
+ */
+ private int mTime;
+ /**
+ * Lower threshold used in auto gain estimation
+ */
+ private int mLowThreshold;
+ /**
+ * Upper threshold used in auto gain estimation
+ */
+ private int mHighThreshold;
+ /**
+ * Value that indicates that the sensor is saturated such that we can not trust the values anymore
+ */
+ private int mClipping;
+
+ IntegrationTime(final int aFieldValue, final int aTime, final int aLowThreshold, final int aHighThreshold,
+ final int aClipping) {
+ mFieldValue = aFieldValue;
+ mTime = aTime;
+ mLowThreshold = aLowThreshold;
+ mHighThreshold = aHighThreshold;
+ mClipping = aClipping;
+ }
+
+ public int getFieldValue() {
+ return mFieldValue;
+ }
+
+ public int getTime() {
+ return mTime;
+ }
+
+ public int getLowThreshold() {
+ return mLowThreshold;
+ }
+
+ public int getHighThreshold() {
+ return mHighThreshold;
+ }
+
+ public int getClipping() {
+ return mClipping;
+ }
+ }
+
+ /**
+ * Enum holding the gain options that are supported by the sensor
+ */
+ enum Gain {
+ X1(0x00), //
+ X16(0x10); //
+
+ private int mFieldValue;
+
+ Gain(final int aFieldValue) {
+ mFieldValue = aFieldValue;
+ }
+
+ public int getFieldValue() {
+ return mFieldValue;
+ }
+ }
+
+ /**
+ * Bit indicating command mode to the sensor
+ */
+ private final int COMMAND_BIT = 0x80;
+
+ /**
+ * Bit indicating that a word should be read/written instead of a byte
+ */
+ private final int WORD_BIT = 0x20;
+
+ /**
+ * The light sensor
+ */
+ private final I2CDevice mSensor;
+
+ /**
+ * If this is set, gain is adjusted automatically to improve the measurement
+ */
+ private boolean mAutoGain;
+
+ private IntegrationTime mIntegrationTime;
+ private Gain mGain;
+ private final Logger mLogger;
+
+ /**
+ * Writes an 8 bit value to the specified register
+ *
+ * @throws IOException
+ */
+ private void writeByte(final int aReg, final int aValue) throws IOException {
+ mLogger.trace("Writing to register 0x" + Integer.toHexString(aReg) + " data 0x" + Integer.toHexString(aValue));
+ mSensor.write((byte) aReg, (byte) aValue);
+ }
+
+ /**
+ * Reads a 16 bit value from the specified register
+ *
+ * @throws IOException
+ */
+ private byte[] readWord(final int aReg) throws IOException {
+ final byte[] result = new byte[2];
+ mSensor.read((byte) aReg, result, 0, 2);
+
+ mLogger.trace("Read from register 0x" + Integer.toHexString(aReg) + " data [0x"
+ + Integer.toHexString(result[0] & 0xFF) + ", 0x" + Integer.toHexString(result[1] & 0xFF) + "]");
+
+ return result;
+ }
+
+ /**
+ * Turn the light sensor on or off
+ *
+ * @throws IOException
+ */
+ private void setPower(final Power aPower) throws IOException {
+ writeByte(COMMAND_BIT | Register.CONTROL.getRegister(), aPower.getFieldValue());
+ }
+
+ /**
+ * @throws IOException
+ */
+ private Luminosity getData() throws IOException {
+ setPower(Power.ON);
+
+ try {
+ // wait for measurement
+ Thread.sleep(mIntegrationTime.getTime());
+ }
+ catch (InterruptedException e) {
+ mLogger.error("Waiting for integration was interrupted");
+ }
+
+ final byte[] broadband = readWord(COMMAND_BIT | WORD_BIT | Register.BROADBAND.getRegister());
+ final byte[] infrared = readWord(COMMAND_BIT | WORD_BIT | Register.INFRARED.getRegister());
+
+ final Luminosity luminosity = new Luminosity(broadband, infrared, mIntegrationTime, mGain);
+ mLogger.debug("Light sensor measured " + luminosity);
+
+ setPower(Power.OFF);
+
+ return luminosity;
+ }
+
+ /**
+ * Constructor, configures the sensor
+ *
+ * @throws IOException
+ */
+ public Tsl2561(final I2CDevice aSensorDevice) throws IOException {
+ mSensor = aSensorDevice;
+ mLogger = LogManager.getLogger();
+
+ mAutoGain = true;
+ mIntegrationTime = IntegrationTime.MS_402;
+ mGain = Gain.X16;
+
+ setIntegrationTime(mIntegrationTime);
+ setGain(mGain);
+
+ setPower(Power.OFF);
+ }
+
+ /**
+ * Enables or disables trying to automatically improve results by adjusting the gain setting
+ */
+ public void setAutoGain(final boolean aAutoGain) {
+ mAutoGain = aAutoGain;
+ }
+
+ public void setIntegrationTime(final IntegrationTime aIntegrationTime) {
+ mLogger.trace("Setting integration time " + aIntegrationTime);
+ try {
+ writeByte(COMMAND_BIT | Register.TIMING.getRegister(), aIntegrationTime.getFieldValue() | mGain.getFieldValue());
+
+ mIntegrationTime = aIntegrationTime;
+ }
+ catch (IOException e) {
+ mLogger.error("Could not set integration time to " + aIntegrationTime + ": " + e.getMessage());
+ }
+ }
+
+ public void setGain(final Gain aGain) {
+ mLogger.trace("Setting gain " + aGain);
+ try {
+ writeByte(COMMAND_BIT | Register.TIMING.getRegister(), mIntegrationTime.getFieldValue() | aGain.getFieldValue());
+
+ mGain = aGain;
+ }
+ catch (IOException e) {
+ mLogger.error("Could not set gain to " + aGain + ": " + e.getMessage());
+ }
+ }
+
+ /**
+ * @throws IOException
+ */
+ private Luminosity getLuminosity() throws IOException {
+ Luminosity luminosity = getData();
+
+ if (mAutoGain) {
+ mLogger.trace("Auto gain is active");
+ if ((luminosity.getBroadband() < mIntegrationTime.getLowThreshold()) && (mGain == Gain.X1)) {
+ mLogger.debug("broadband value too low, increasing gain");
+ setGain(Gain.X16);
+ luminosity = getData();
+ }
+ else if ((luminosity.getBroadband() > mIntegrationTime.getHighThreshold()) && (mGain == Gain.X16)) {
+ mLogger.debug("broadband value too high, decreasing gain");
+ setGain(Gain.X1);
+ luminosity = getData();
+ }
+ }
+ else {
+ mLogger.trace("Auto gain is deactivated");
+ }
+
+ return luminosity;
+ }
+
+ public double getLux() throws IOException {
+ Luminosity luminosity = getLuminosity();
+
+ if (luminosity.getBroadband() == 0 && luminosity.getInfrared() == 0) {
+ luminosity = getLuminosity();
+ }
+
+ final double lux = luminosity.calculateLux();
+
+ mLogger.info("Calculated " + lux + " lux from raw values " + luminosity);
+
+ return lux;
+ }
+}
\ No newline at end of file
diff --git a/src/sample/Main.java b/src/sample/Main.java
new file mode 100644
index 0000000..537bc1f
--- /dev/null
+++ b/src/sample/Main.java
@@ -0,0 +1,360 @@
+package sample;
+
+import javafx.animation.*;
+import javafx.application.Application;
+import javafx.application.Platform;
+import javafx.beans.property.BooleanProperty;
+import javafx.beans.property.SimpleBooleanProperty;
+import javafx.scene.Group;
+import javafx.scene.Scene;
+import javafx.scene.control.*;
+import javafx.scene.effect.ColorAdjust;
+import javafx.scene.image.Image;
+import javafx.scene.image.ImageView;
+import javafx.scene.input.KeyCode;
+import javafx.scene.input.KeyEvent;
+import javafx.scene.layout.StackPane;
+import javafx.scene.paint.Color;
+import javafx.scene.shape.Rectangle;
+import javafx.scene.shape.StrokeType;
+import javafx.scene.text.Font;
+import javafx.stage.Stage;
+import javafx.util.Duration;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.stream.IntStream;
+
+public class Main extends Application {
+
+ static final int SCALE = 2;
+ static final int SPRITE_SIZE = 32;
+ static final int CELL_SIZE = SPRITE_SIZE * SCALE;
+ static final int HORIZONTAL_CELLS = 12;
+ static final int VERTICAL_CELLS = 7;
+ static final int BOARD_WIDTH = HORIZONTAL_CELLS * CELL_SIZE;
+ static final int BOARD_HEIGHT = VERTICAL_CELLS * CELL_SIZE;
+ public static boolean gameover = false;
+ private static int anInt;
+ private static ImageView sun;
+ private ImageView background;
+ public static List sprites = new ArrayList<>();
+ public static Group root;
+ public static Font pixelated = Font.loadFont(Main.class.getResourceAsStream("fonts/pixelated.ttf"), Main.CELL_SIZE);
+ public static Group spriteGroup = new Group();
+ public static SpriteView.Phippy phippy;
+ public static SpriteView.CaptainKube captainKube;
+ public static BooleanProperty earthquake = new SimpleBooleanProperty(false);
+ private static Label messageDisplay;
+ private static Label pokemonCounter;
+ private static int pokemonCaught = 0;
+ private static Timeline clearMessageDisplay = new Timeline(new KeyFrame(Duration.seconds(5)));
+ private static ColorAdjust colorAdjustment;
+
+ @Override
+ public void start(Stage primaryStage) throws Exception {
+ primaryStage.setTitle("PokeTime");
+ StackPane wrapper = new StackPane();
+ root = new Group();
+ Scene scene = new Scene(root, BOARD_WIDTH, BOARD_HEIGHT, Color.BLACK);
+ primaryStage.setScene(scene);
+ populateBackground(root);
+ messageDisplay = new Label();
+ pokemonCounter = new Label();
+
+ // Create Phippy
+ phippy = new SpriteView.Phippy(new Location(0, 3));
+
+ // Add Captain Kube
+ captainKube = new SpriteView.CaptainKube(new Location(2, 3));
+ spriteGroup.getChildren().add(captainKube);
+
+ // Create Phippy's Friends
+ SpriteView.Goldie goldie = new SpriteView.Goldie(30, 50);
+ spriteGroup.getChildren().add(goldie);
+ SpriteView.Zee zee = new SpriteView.Zee(80, 59);
+ spriteGroup.getChildren().add(zee);
+ SpriteView.Linky linky = new SpriteView.Linky(140, 55);
+ spriteGroup.getChildren().add(linky);
+ SpriteView.Hazel hazel = new SpriteView.Hazel(44, 80);
+ spriteGroup.getChildren().add(hazel);
+ SpriteView.Tiago tiago = new SpriteView.Tiago(106, 92);
+ spriteGroup.getChildren().add(tiago);
+ SpriteView.Owlina owlina = new SpriteView.Owlina(155, 85);
+ spriteGroup.getChildren().add(owlina);
+
+ // Add some pods
+ spriteGroup.getChildren().add(new SpriteView.PodSitter(new Location(8, 2), goldie,false));
+ spriteGroup.getChildren().add(new SpriteView.PodSitter(new Location(6, 6), zee,true));
+ spriteGroup.getChildren().add(new SpriteView.PodCarrier(new Location(9, 4), linky,false));
+ spriteGroup.getChildren().add(new SpriteView.PodCarrier(new Location(5, 1), hazel,true));
+ spriteGroup.getChildren().add(new SpriteView.PodBalloon(new Location(7, 6), tiago, false));
+ spriteGroup.getChildren().add(new SpriteView.PodBalloon(new Location(9, 4), owlina, true));
+
+ // Load the Sun
+ sun = new ImageView(new Image(Main.class.getResource("images/sun.png").toString(), Main.CELL_SIZE * Main.SCALE, Main.CELL_SIZE * Main.SCALE, true, false));
+
+ populateCells(root, phippy);
+ root.getChildren().add(spriteGroup);
+ spriteGroup.getChildren().add(phippy);
+ addKeyHandler(scene, phippy);
+ phippy.idle = true;
+
+ colorAdjustment = new ColorAdjust();
+ background.setEffect(colorAdjustment);
+ root.getChildren().add(messageDisplay);
+ messageDisplay.setFont(pixelated);
+ messageDisplay.setLayoutX(CELL_SIZE / 4);
+ messageDisplay.setLayoutY(BOARD_HEIGHT - CELL_SIZE * 1.7);
+ messageDisplay.setTextFill(Color.WHITESMOKE);
+ clearMessageDisplay.setOnFinished((ae) -> messageDisplay.setText(""));
+
+ root.getChildren().add(pokemonCounter);
+ pokemonCounter.setFont(pixelated);
+ pokemonCounter.setLayoutX(CELL_SIZE / 4);
+ pokemonCounter.setLayoutY(CELL_SIZE / 4);
+ pokemonCounter.setTextFill(Color.DARKGREEN);
+
+ SensorFactory sensorFactory = SensorFactory.create();
+ sensorFactory.createButton();
+ sensorFactory.createLightSensor();
+ sensorFactory.createAccelerometer();
+
+ primaryStage.show();
+ }
+
+ private void populateBackground(Group root) {
+ background = new ImageView(new Image(getClass().getResource("images/island_background.png").toString(), 768, 448, true, false));
+ background.setFitHeight(BOARD_HEIGHT);
+ background.setFitWidth(BOARD_WIDTH);
+ root.getChildren().add(background);
+ }
+
+ private void populateCells(Group root, final SpriteView mainCharacter) {
+ // Gratuitous use of lambdas to do nested iteration!
+ Group cells = new Group();
+ IntStream.range(0, HORIZONTAL_CELLS).mapToObj(i ->
+ IntStream.range(0, VERTICAL_CELLS).mapToObj(j -> {
+ Rectangle rect = new Rectangle(i * CELL_SIZE, j * CELL_SIZE, CELL_SIZE, CELL_SIZE);
+ rect.setFill(Color.rgb(0, 0, 0, 0));
+ rect.setStrokeType(StrokeType.INSIDE);
+ rect.setStroke(Color.BLACK);
+ rect.setOnMousePressed(e -> mainCharacter.move(mainCharacter.location.get().directionTo(new Location(i, j))));
+ return rect;
+ })
+ ).flatMap(s -> s).forEach(cells.getChildren()::add);
+ root.getChildren().add(cells);
+ }
+
+ private void addKeyHandler(Scene scene, SpriteView mary) {
+ scene.addEventHandler(KeyEvent.KEY_PRESSED, ke -> {
+ KeyCode keyCode = ke.getCode();
+ switch (keyCode) {
+ case W:
+ case UP:
+ mary.move(Direction.UP);
+ break;
+ case A:
+ case LEFT:
+ mary.move(Direction.LEFT);
+ break;
+ case S:
+ case DOWN:
+ mary.move(Direction.DOWN);
+ break;
+ case D:
+ case RIGHT:
+ mary.move(Direction.RIGHT);
+ break;
+ case Z:
+ if (ke.isControlDown() && ke.isShiftDown())
+ whistle();
+ break;
+ case X:
+ if (ke.isControlDown() && ke.isShiftDown())
+ sun();
+ break;
+ case C:
+ if (ke.isControlDown() && ke.isShiftDown())
+ earthquake();
+ break;
+ case ESCAPE:
+ System.exit(0);
+ }
+ });
+ }
+
+ public static void main(String[] args) {
+ launch(args);
+ }
+ private static SpriteView.Pod enemy;
+
+ public static void whistle() {
+ Platform.runLater(() -> whistleImpl());
+ }
+
+ private static void whistleImpl() {
+ Main.phippy.playAnimation();
+ for (SpriteView sprite : sprites) {
+ if (sprite instanceof SpriteView.PodSitter) {
+ SpriteView.PodSitter podSitter = (SpriteView.PodSitter) sprite;
+ sprite.moveTo(phippy.getLocation());
+ if (!phippy.getAnimals().contains(podSitter)) {
+ phippy.getAnimals().add(podSitter);
+ }
+ podSitter.stop();
+ podSitter.continuousWalk();
+ }
+ }
+ }
+
+ public static void sun() {
+ Main.root.getChildren().add(sun);
+ Timeline sunTimeline = new Timeline(new KeyFrame[] {new KeyFrame(Duration.seconds(2), new KeyValue(colorAdjustment.brightnessProperty(), 0.6)),
+ new KeyFrame(Duration.seconds(4), new KeyValue(colorAdjustment.brightnessProperty(), 0))});
+ sunTimeline.onFinishedProperty().set(event -> {
+ Main.root.getChildren().remove(sun);
+ for (SpriteView sprite : sprites) {
+ if (sprite instanceof SpriteView.PodBalloon) {
+ SpriteView.PodBalloon podBalloon = (SpriteView.PodBalloon) sprite;
+ podBalloon.stop();
+ podBalloon.playAnimation();
+ }
+ }
+ });
+ sunTimeline.play();
+ }
+
+ public static void earthquake() {
+ earthquake.setValue(true);
+ Timeline quakeTimeline = new Timeline();
+ for (int i = 1; i < 20; i++) {
+ quakeTimeline.getKeyFrames().add(new KeyFrame(Duration.seconds(.1 * i), new KeyValue(spriteGroup.translateXProperty(), i % 2 == 0 ? -10 : 10)));
+ }
+ quakeTimeline.getKeyFrames().add(new KeyFrame(Duration.seconds(2), new KeyValue(spriteGroup.translateXProperty(), 0)));
+ quakeTimeline.setOnFinished((o) -> {
+ earthquake.setValue(false);
+ for (SpriteView sprite : sprites) {
+ if (sprite instanceof SpriteView.Pod.PodCarrier || sprite instanceof SpriteView.Pod.PodBalloon) {
+ SpriteView.Pod pod = (SpriteView.Pod) sprite;
+ pod.stop();
+ pod.playAnimation();
+ }
+ }
+ });
+ quakeTimeline.play();
+ }
+
+ public static void display(String message) {
+ Platform.runLater(() -> {
+ messageDisplay.setText(message);
+ clearMessageDisplay.playFromStart();
+ });
+ }
+
+ public static enum Direction {
+ DOWN(0), LEFT(1), RIGHT(2), UP(3), ANIMATE1(4), ANIMATE2(5), ANIMATE3(6), ANIMATE4(7);
+ private final int offset;
+ Direction(int offset) {
+ this.offset = offset;
+ }
+ public int getOffset() {
+ return offset;
+ }
+ public int getXOffset() {
+ switch (this) {
+ case LEFT:
+ return -1;
+ case RIGHT:
+ return 1;
+ default:
+ return 0;
+ }
+ }
+ public int getYOffset() {
+ switch (this) {
+ case UP:
+ return -1;
+ case DOWN:
+ return 1;
+ default:
+ return 0;
+ }
+ }
+ public static Direction random() {
+ switch ((int)(4 * Math.random())) {
+ case 0:
+ return DOWN;
+ case 1:
+ return LEFT;
+ case 2:
+ return RIGHT;
+ default:
+ return UP;
+ }
+ }
+ }
+
+ public static class Location {
+ int cell_x;
+ int cell_y;
+ public Location(int cell_x, int cell_y) {
+ this.cell_x = cell_x;
+ this.cell_y = cell_y;
+ }
+ public int getX() {
+ return cell_x;
+ }
+ public int getY() {
+ return cell_y;
+ }
+ public Location offset(int x, int y) {
+ return new Location(cell_x + x, cell_y + y);
+ }
+ public Direction directionTo(Location loc) {
+ if (Math.abs(loc.cell_x - cell_x) > Math.abs(loc.cell_y - cell_y)) {
+ return (loc.cell_x > cell_x) ? Direction.RIGHT : Direction.LEFT;
+ } else {
+ return (loc.cell_y > cell_y) ? Direction.DOWN : Direction.UP;
+ }
+ }
+ public Direction directionFrom(Location loc) {
+ if (Math.abs(loc.cell_x - cell_x) < Math.abs(loc.cell_y - cell_y)) {
+ return (loc.cell_x > cell_x) ? Direction.LEFT : Direction.RIGHT;
+ } else {
+ return (loc.cell_y > cell_y) ? Direction.UP : Direction.DOWN;
+ }
+ }
+ public int distance(Location loc) {
+ return (Math.abs(loc.cell_x - cell_x) + Math.abs(loc.cell_y - cell_y)) / 2;
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) return true;
+ if (o == null || getClass() != o.getClass()) return false;
+
+ Location location = (Location) o;
+
+ if (cell_x != location.cell_x) return false;
+ return cell_y == location.cell_y;
+
+ }
+
+ @Override
+ public int hashCode() {
+ int result = cell_x;
+ result = 31 * result + cell_y;
+ return result;
+ }
+
+ @Override
+ public String toString() {
+ return "Location{" +
+ "cell_x=" + cell_x +
+ ", cell_y=" + cell_y +
+ '}';
+ }
+ }
+
+}
diff --git a/src/sample/MultiplierClock.java b/src/sample/MultiplierClock.java
new file mode 100644
index 0000000..ff552d2
--- /dev/null
+++ b/src/sample/MultiplierClock.java
@@ -0,0 +1,27 @@
+package sample;
+
+import java.time.*;
+
+public class MultiplierClock extends Clock {
+ private final Instant creationTime;
+ private Clock base;
+ private final long multiplier;
+ public MultiplierClock(Clock base, long multiplier) {
+ this.base = base;
+ this.multiplier = multiplier;
+ creationTime = base.instant();
+ }
+ @Override
+ public ZoneId getZone() {
+ return base.getZone();
+ }
+ @Override
+ public Clock withZone(ZoneId zone) {
+ return base.withZone(zone);
+ }
+ @Override
+ public Instant instant() {
+ Instant now = base.instant();
+ return creationTime.plus(Duration.between(creationTime, now).multipliedBy(multiplier));
+ }
+}
diff --git a/src/sample/PiSystem.java b/src/sample/PiSystem.java
new file mode 100644
index 0000000..0cd5720
--- /dev/null
+++ b/src/sample/PiSystem.java
@@ -0,0 +1,65 @@
+package sample;
+
+import java.io.BufferedReader;
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.InputStreamReader;
+
+/**
+ * Created by Cassandra on 8/20/2017.
+ */
+public class PiSystem {
+ private static boolean isWindows = false;
+ private static boolean isLinux = false;
+ private static boolean isHpUnix = false;
+ public static boolean isPiUnix = false;
+ private static boolean isSolaris = false;
+ private static boolean isSunOS = false;
+ private static boolean archDataModel32 = false;
+ private static boolean archDataModel64 = false;
+
+ static {
+ final String os = System.getProperty("os.name").toLowerCase();
+ if (os.indexOf("windows") >= 0) {
+ isWindows = true;
+ }
+ if (os.indexOf("linux") >= 0) {
+ isLinux = true;
+ }
+ if (os.indexOf("hp-ux") >= 0) {
+ isHpUnix = true;
+ }
+ if (os.indexOf("hpux") >= 0) {
+ isHpUnix = true;
+ }
+ if (os.indexOf("solaris") >= 0) {
+ isSolaris = true;
+ }
+ if (os.indexOf("sunos") >= 0) {
+ isSunOS = true;
+ }
+ if (System.getProperty("sun.arch.data.model").equals("32")) {
+ archDataModel32 = true;
+ }
+ if (System.getProperty("sun.arch.data.model").equals("64")) {
+ archDataModel64 = true;
+ }
+ if (isLinux) {
+ final File file = new File("/etc", "os-release");
+ try (FileInputStream fis = new FileInputStream(file);
+ BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(fis))) {
+ String string;
+ while ((string = bufferedReader.readLine()) != null) {
+ if (string.toLowerCase().contains("raspbian")) {
+ if (string.toLowerCase().contains("name")) {
+ isPiUnix = true;
+ break;
+ }
+ }
+ }
+ } catch (final Exception e) {
+ e.printStackTrace();
+ }
+ }
+ }
+}
diff --git a/src/sample/SensorFactory.java b/src/sample/SensorFactory.java
new file mode 100644
index 0000000..39e0854
--- /dev/null
+++ b/src/sample/SensorFactory.java
@@ -0,0 +1,114 @@
+package sample;
+
+import com.pi4j.component.gyroscope.analogdevices.ADXL345;
+import com.pi4j.io.gpio.*;
+import com.pi4j.io.gpio.event.GpioPinDigitalStateChangeEvent;
+import com.pi4j.io.gpio.event.GpioPinListenerDigital;
+import com.pi4j.io.i2c.I2CBus;
+import com.pi4j.io.i2c.I2CDevice;
+import com.pi4j.io.i2c.I2CFactory;
+import javafx.animation.KeyFrame;
+import javafx.animation.Timeline;
+import javafx.beans.property.BooleanProperty;
+import javafx.util.Duration;
+import joachimeichborn.sensors.driver.Tsl2561;
+
+import java.io.IOException;
+
+/**
+ * Created by Cassandra on 9/9/2016.
+ */
+public class SensorFactory {
+ private static SensorFactory factory;
+ private GpioController gpio;
+ private I2CBus bus;
+ private float lastGyroX;
+
+ public static SensorFactory create() throws IOException, I2CFactory.UnsupportedBusNumberException {
+ if (factory == null) {
+ factory = new SensorFactory();
+ }
+ return factory;
+ }
+
+ public SensorFactory() throws IOException, I2CFactory.UnsupportedBusNumberException {
+ if (PiSystem.isPiUnix) {
+ gpio = GpioFactory.getInstance();
+ bus = I2CFactory.getInstance(I2CBus.BUS_1);
+ }
+ }
+
+ public void createButton() {
+ if (PiSystem.isPiUnix) {
+ final GpioPinDigitalInput myButton = gpio.provisionDigitalInputPin(RaspiPin.GPIO_07, PinPullResistance.PULL_UP);
+ myButton.addListener(new GpioPinListenerDigital() {
+ @Override
+ public void handleGpioPinDigitalStateChangeEvent(GpioPinDigitalStateChangeEvent event) {
+ boolean buttonPressed = event.getState().isLow();
+ if (buttonPressed) Main.display("Button Pressed");
+ // Call the pods to Phippy
+ }
+ });
+ }
+ }
+
+ public void createLightSensor() throws IOException {
+ if (PiSystem.isPiUnix) {
+ I2CDevice device = bus.getDevice(0x39);
+ try {
+ Tsl2561 lightSensor = new Tsl2561(device);
+ Timeline lightTimeline = new Timeline(new KeyFrame(Duration.seconds(10), actionEvent -> {
+ try {
+ double lux = lightSensor.getLux();
+ Main.display("lux = " + lux);
+ // Bring out the sun!
+ } catch (IOException e) {
+ e.printStackTrace();
+ }
+ }));
+ lightTimeline.setCycleCount(Timeline.INDEFINITE);
+ lightTimeline.play();
+ } catch (IOException e) {
+ System.out.println("Light Sensor is probably not connected... " + e.getMessage());
+ }
+ }
+ }
+
+ public void createAccelerometer() {
+ if (PiSystem.isPiUnix) {
+ try {
+ ADXL345 gyro = new ADXL345(bus);
+ gyro.init(gyro.X, 4);
+ lastGyroX = gyro.X.getRawValue();
+ Timeline accelerometerTimeline = new Timeline(new KeyFrame(Duration.seconds(1), actionEvent -> {
+ try {
+ float x = gyro.X.getRawValue();
+ if (!Main.earthquake.getValue()) {
+ if (Math.abs(x - lastGyroX) > 2000) {
+ Main.display("Earthquake!");
+ // Make an earthquake!
+ }
+ }
+ lastGyroX = x;
+ } catch (IOException e) {
+ e.printStackTrace();
+ }
+ }));
+ accelerometerTimeline.setCycleCount(Timeline.INDEFINITE);
+ accelerometerTimeline.play();
+ } catch (IOException e) {
+ System.out.println("Accelerometer is probably not connected... " + e.getMessage());
+ }
+ }
+ }
+}
+// Call the pods to Phippy
+// Main.whistle();
+
+// Bring out the sun!
+// if (lux > 100) {
+// Main.sun();
+// }
+
+// Make an earthquake!
+// Main.earthquake();
diff --git a/src/sample/SpriteView.java b/src/sample/SpriteView.java
new file mode 100644
index 0000000..6590e95
--- /dev/null
+++ b/src/sample/SpriteView.java
@@ -0,0 +1,451 @@
+package sample;
+
+import javafx.animation.*;
+import javafx.application.Platform;
+import javafx.beans.property.*;
+import javafx.beans.value.ChangeListener;
+import javafx.collections.FXCollections;
+import javafx.collections.ListChangeListener;
+import javafx.collections.ObservableList;
+import javafx.event.ActionEvent;
+import javafx.event.EventHandler;
+import javafx.geometry.Pos;
+import javafx.geometry.Rectangle2D;
+import javafx.scene.Group;
+import javafx.scene.Node;
+import javafx.scene.control.Label;
+import javafx.scene.effect.ColorAdjust;
+import javafx.scene.image.Image;
+import javafx.scene.image.ImageView;
+import javafx.scene.layout.StackPane;
+import javafx.scene.paint.Color;
+import javafx.scene.shape.Rectangle;
+import javafx.util.Duration;
+
+import java.util.ArrayList;
+
+public class SpriteView extends StackPane {
+ protected final ImageView imageView;
+ private Color color;
+ EventHandler arrivalHandler;
+ double colorOffset;
+ private int spritesX;
+ private boolean continuousWalking = false;
+
+ public void setDirection(Main.Direction direction) {
+ this.direction.setValue(direction);
+ }
+
+ public static class Goldie extends HiddenFriend {
+ public Goldie(int x, int y) {
+ super("goldie", x, y, 1);
+ }
+ }
+
+ public static class Zee extends HiddenFriend {
+ public Zee(int x, int y) {
+ super("zee", x, y, 1.4);
+ }
+ }
+
+ public static class Linky extends HiddenFriend {
+ public Linky(int x, int y) {
+ super("linky", x, y, 1.2);
+ }
+ }
+
+ public static class Hazel extends HiddenFriend {
+ public Hazel(int x, int y) {
+ super("hazel", x, y, 1.2);
+ }
+ }
+
+ public static class Tiago extends HiddenFriend {
+ public Tiago(int x, int y) {
+ super("tiago", x, y, 1.2);
+ }
+ }
+
+ public static class Owlina extends HiddenFriend {
+ public Owlina(int x, int y) {
+ super("owlina", x, y, 1);
+ }
+ }
+
+ public static class HiddenFriend extends Friend {
+ public HiddenFriend(String name, int x, int y, double scale) {
+ super(name, new Main.Location(0, 0), 1);
+ setTranslateX(x);
+ setTranslateY(y);
+ setVisible(false);
+ }
+ }
+
+ public static class CaptainKube extends Friend {
+ public CaptainKube(Main.Location loc) {
+ super("captainkube", loc, 1.5);
+ }
+ }
+
+ public static class Friend extends SpriteView {
+ public Friend(String name, Main.Location loc, double scale) {
+ super(loadImage("images/" + name + ".png", 1, 1, scale), loc, 1, 1, 1);
+ direction.set(Main.Direction.DOWN);
+ frame.set(0);
+ }
+ }
+
+ public static class PodSitter extends Pod {
+ public PodSitter(Main.Location loc, Friend friend, boolean alternateColor) {
+ super("podsitter", loc, friend, alternateColor, 7, 4, 1.4, 1);
+ }
+ }
+ public static class PodCarrier extends Pod {
+ public PodCarrier(Main.Location loc, Friend friend, boolean alternateColor) {
+ super("podcarrier", loc, friend, alternateColor, 3, 7, 1.85, 2);
+ avoid = Main.phippy;
+ }
+ }
+ public static class PodBalloon extends Pod {
+ public PodBalloon(Main.Location loc, Friend friend, boolean alternateColor) {
+ super("podballoon", loc, friend, alternateColor, 6, 8,1.8, 1);
+ avoid = Main.phippy;
+ }
+ }
+ protected boolean inBounds(Main.Direction direction) {
+ Main.Location loc = location.getValue().offset(direction.getXOffset(), direction.getYOffset());
+ return (loc.cell_x >= 0) && (loc.cell_x < Main.HORIZONTAL_CELLS) && (loc.cell_y >= 0) && (loc.cell_y < Main.VERTICAL_CELLS);
+ }
+ public static class Pod extends RandomWalker {
+ private String name;
+ private Friend friend;
+
+ public Pod(String name, Main.Location loc, Friend friend, boolean alternateColor, int spritesX, int spritesY, double scaleFactor, double speed) {
+ super(loadImage("images/" + name + (alternateColor ? "2" : "") + ".png", spritesX, spritesY, scaleFactor), loc, spritesX, spritesY, speed);
+ this.name = Character.toUpperCase(name.charAt(0)) + name.substring(1);
+ this.friend = friend;
+ }
+
+ public String getName() {
+ return name;
+ }
+
+ public void displayFriend() {
+ friend.setVisible(true);
+ }
+ }
+ public static class Phippy extends Shepherd {
+ int animalsReturned = 0;
+ static final Image PHIPPY = loadImage("images/phippy.png", 3, 8, 2);
+ public Phippy(Main.Location loc) {
+ super(PHIPPY, loc, 3, 5);
+ arrivalHandler = e -> {
+ if (Main.captainKube.location.get().equals(location.get())) {
+ System.out.println("Returning pods to the ship");
+ for (SpriteView animal : getAnimals()) {
+ Main.root.getChildren().remove(animal);
+ Main.spriteGroup.getChildren().remove(animal);
+ Main.sprites.remove(animal);
+ ((Pod)animal).displayFriend();
+ animalsReturned++;
+ }
+ getAnimals().clear();
+ };
+ if ((!Main.gameover) && (animalsReturned >= 6)) {
+ win();
+ }
+ for (SpriteView sprite : Main.sprites) {
+ if (sprite.getLocation().equals(location.get())) {
+ if (sprite instanceof PodBalloon) {
+ System.out.println("Picking up a pod balloon");
+ if (!getAnimals().contains(sprite)) {
+ getAnimals().add(sprite);
+ }
+ } else if (sprite instanceof PodCarrier) {
+ System.out.println("Picking up a pod carrier");
+ if (!getAnimals().contains(sprite)) {
+ getAnimals().add(sprite);
+ }
+ }
+ }
+ }
+ };
+ }
+ public void die() {
+ Main.gameover = true;
+ RotateTransition rotate = new RotateTransition(Duration.seconds(3), Phippy.this);
+ rotate.byAngleProperty().set(1080);
+ rotate.setOnFinished(actionEvent -> Main.root.getChildren().remove(Phippy.this));
+ rotate.play();
+ Main.sprites.remove(this);
+ Main.root.getChildren().add(new Rectangle(Main.BOARD_WIDTH, Main.BOARD_HEIGHT, Color.color(0, 0, 0, .4)));
+ Label label = new Label("GAME OVER");
+ label.setTextFill(Color.WHITESMOKE);
+ label.setAlignment(Pos.BASELINE_CENTER);
+ label.setFont(Main.pixelated);
+ label.setPrefHeight(Main.BOARD_HEIGHT);
+ label.setPrefWidth(Main.BOARD_WIDTH);
+ Main.root.getChildren().add(label);
+ }
+ public void win() {
+ Main.gameover = true;
+ Main.root.getChildren().add(new Rectangle(Main.BOARD_WIDTH, Main.BOARD_HEIGHT, Color.color(0, 0, 0, .4)));
+ Label label = new Label("YOU WIN!!!");
+ label.setTextFill(Color.LIGHTGREEN);
+ label.setAlignment(Pos.BASELINE_CENTER);
+ label.setFont(Main.pixelated);
+ label.setPrefHeight(Main.BOARD_HEIGHT);
+ label.setPrefWidth(Main.BOARD_WIDTH);
+ Main.root.getChildren().add(label);
+ }
+ }
+
+ public static class RandomWalker extends SpriteView {
+ protected Timeline walk;
+ protected boolean idle = false;
+ protected Main.Location target;
+ protected SpriteView avoid;
+ public RandomWalker(Image spriteSheet, Main.Location loc) {
+ this(spriteSheet, loc, 3, 4, 1);
+ }
+ public RandomWalker(Image spriteSheet, Main.Location loc, int spritesX, int spritesY, double speed) {
+ super(spriteSheet, loc, spritesX, spritesY, speed);
+ walk = new Timeline(new KeyFrame(Duration.seconds(.2), actionEvent -> {
+ if (idle) return;
+ if (target != null) {
+ move(getLocation().directionTo(target));
+ } else if (avoid != null && (getLocation().distance(avoid.location.get()) < 2)) {
+ move(getLocation().directionFrom(avoid.location.get()));
+ } else {
+ Main.Direction random = Main.Direction.random();
+ if (inBounds(random)) {
+ move(random);
+ }
+ }
+ }));
+ walk.setCycleCount(Timeline.INDEFINITE);
+ walk.play();
+ Main.earthquake.addListener((observable, oldValue, earthquake) -> {
+ if (earthquake) {
+ stop();
+ } else {
+ play();
+ }
+ });
+ }
+ public void stop() {
+ walk.stop();
+ }
+ public void play() {
+ walk.play();
+ }
+ }
+
+ public static class Shepherd extends RandomWalker {
+ private ObservableList animals;
+ public ObservableList getAnimals() {
+ return animals;
+ }
+ public Shepherd(Image spriteSheet, Main.Location loc) {
+ this(spriteSheet, loc, 3, 4);
+ }
+ public Shepherd(Image spriteSheet, Main.Location loc, int spritesX, int spritesY) {
+ super(spriteSheet, loc, spritesX, spritesY, 1);
+ animals = FXCollections.observableArrayList();
+ animals.addListener((ListChangeListener) c -> {
+ ObservableList children = ((Group) getParent()).getChildren();
+ while (c.next()) {
+ if (c.wasAdded() || c.wasRemoved() || c.wasReplaced()) {
+ SpriteView prev = this;
+ int number = 0;
+ for (SpriteView a : animals) {
+ a.following = prev;
+ a.number.set(++number);
+ prev.follower = a;
+ prev = a;
+ }
+ }
+ }
+ });
+ }
+ public void move(Main.Direction direction) {
+ if (walking != null && walking.getStatus().equals(Animation.Status.RUNNING))
+ return;
+ if (!inBounds(direction))
+ return;
+ Main.Location myOldLoc = location.get();
+ moveTo(location.getValue().offset(direction.getXOffset(), direction.getYOffset()));
+ animals.stream().reduce(myOldLoc,
+ (loc, sprt) -> {
+ Main.Location oldLoc = sprt.location.get();
+ sprt.moveTo(loc);
+ return oldLoc;
+ }, (loc1, loc2) -> loc1
+ );
+ }
+ }
+
+ public static class Ghoul extends SpriteView {
+ static final Image GHOUL = loadImage("images/ghoul.png");
+ public Ghoul(SpriteView following) {
+ super(GHOUL, following);
+ }
+ }
+
+ private SpriteView following;
+ IntegerProperty number = new SimpleIntegerProperty();
+ public int getNumber() {
+ return number.get();
+ }
+ public SpriteView(Image spriteSheet, SpriteView following) {
+ this(spriteSheet, following.getLocation().offset(-following.getDirection().getXOffset(), -following.getDirection().getYOffset()));
+ number.set(following.number.get() + 1);
+ this.following = following;
+ setDirection(following.getDirection());
+ following.follower = this;
+ setMouseTransparent(true);
+ }
+ public SpriteView getFollowing() {
+ return following;
+ }
+
+ ObjectProperty direction = new SimpleObjectProperty<>();
+ ObjectProperty location = new SimpleObjectProperty<>();
+ IntegerProperty frame = new SimpleIntegerProperty(1);
+ int spriteWidth;
+ int spriteHeight;
+ Timeline walking;
+ SpriteView follower;
+ private int spritesY;
+ double speed;
+
+ static Image loadImage(String url) {
+ return loadImage(url, 3, 4);
+ }
+ static Image loadImage(String url, int spritesX, int spritesY) {
+ return loadImage(url, spritesX, spritesY, 1);
+ }
+ static Image loadImage(String url, int spritesX, int spritesY, double scale) {
+ return new Image(SpriteView.class.getResource(url).toString(), Main.SPRITE_SIZE * spritesX * Main.SCALE * scale, Main.SPRITE_SIZE * spritesY * Main.SCALE * scale, true, false);
+ }
+ public SpriteView(Image spriteSheet, Main.Location loc) {
+ this(spriteSheet, loc, 3, 4, 1);
+ }
+ public SpriteView(Image spriteSheet, Main.Location loc, int spritesX, int spritesY, double speed) {
+ this.spritesX = spritesX;
+ this.spritesY = spritesY;
+ this.speed = speed;
+ imageView = new ImageView(spriteSheet);
+ this.location.set(loc);
+ Main.sprites.add(this);
+ spriteWidth = (int) (spriteSheet.getWidth() / spritesX);
+ spriteHeight = (int) (spriteSheet.getHeight() / spritesY);
+ setTranslateX(loc.getX() * Main.CELL_SIZE + (Main.CELL_SIZE - spriteWidth) / 2);
+ setTranslateY(loc.getY() * Main.CELL_SIZE + (Main.CELL_SIZE - spriteHeight));
+ // shouldn't need to subtract or add Main.SCALE
+ ChangeListener