From 5c19c3133cb5a7f38b664778857358309f99c1d9 Mon Sep 17 00:00:00 2001 From: EpicKnarvik97 Date: Sun, 13 Aug 2023 14:24:53 +0200 Subject: [PATCH] Changes a bunch of code Renames some badly named interfaces and classes Changes a bunch of packages for better structure Adds some missing comments Improves path finding performance somewhat Adds an NPC death sound Makes fake walls noticeable Makes all NPCs drop their items when they die (as the NPCs may steal items from a player) --- README.md | 1 + src/main/java/inf101/v18/gfx/IPaintLayer.java | 58 +- src/main/java/inf101/v18/gfx/Screen.java | 1223 ++++++------ .../inf101/v18/gfx/gfxmode/Direction.java | 348 ++-- .../java/inf101/v18/gfx/gfxmode/Gravity.java | 12 +- .../java/inf101/v18/gfx/gfxmode/IPainter.java | 72 +- .../java/inf101/v18/gfx/gfxmode/IShape.java | 404 ++-- .../java/inf101/v18/gfx/gfxmode/ITurtle.java | 464 +++-- .../java/inf101/v18/gfx/gfxmode/Point.java | 197 +- .../inf101/v18/gfx/gfxmode/ShapePainter.java | 553 ++--- .../inf101/v18/gfx/gfxmode/TurtlePainter.java | 639 +++--- .../v18/gfx/textmode/BlocksAndBoxes.java | 362 ++-- .../v18/gfx/textmode/ControlSequences.java | 495 ++--- .../inf101/v18/gfx/textmode/DemoPages.java | 276 +-- .../java/inf101/v18/gfx/textmode/Printer.java | 1688 ++++++++-------- .../inf101/v18/gfx/textmode/TextFont.java | 1772 ++++++++--------- .../v18/gfx/textmode/TextFontAdjuster.java | 402 ++-- .../inf101/v18/gfx/textmode/TextMode.java | 275 +-- .../java/inf101/v18/grid/GridDirection.java | 117 +- src/main/java/inf101/v18/grid/IArea.java | 250 ++- src/main/java/inf101/v18/grid/IGrid.java | 357 ++-- src/main/java/inf101/v18/grid/ILocation.java | 107 - src/main/java/inf101/v18/grid/IMultiGrid.java | 350 ++-- src/main/java/inf101/v18/grid/IPosition.java | 141 +- src/main/java/inf101/v18/grid/Location.java | 104 + src/main/java/inf101/v18/grid/MultiGrid.java | 12 +- src/main/java/inf101/v18/grid/MyGrid.java | 371 ++-- src/main/java/inf101/v18/grid/README-GRID.md | 22 +- src/main/java/inf101/v18/grid/RectArea.java | 576 +++--- .../java/inf101/v18/rogue101/AppInfo.java | 50 +- .../java/inf101/v18/rogue101/Launcher.java | 2 + src/main/java/inf101/v18/rogue101/Main.java | 341 ++-- .../inf101/v18/rogue101/enemies/Boss.java | 64 +- .../inf101/v18/rogue101/enemies/Enemy.java | 46 + .../inf101/v18/rogue101/enemies/Girl.java | 190 +- .../inf101/v18/rogue101/events/GameEvent.java | 198 +- .../inf101/v18/rogue101/events/IEvent.java | 85 +- .../inf101/v18/rogue101/examples/Carrot.java | 128 +- .../v18/rogue101/examples/ExampleItem.java | 51 - .../inf101/v18/rogue101/examples/Rabbit.java | 195 +- .../java/inf101/v18/rogue101/game/Game.java | 1055 +++------- .../java/inf101/v18/rogue101/game/IGame.java | 340 ---- .../rogue101/game/IllegalMoveException.java | 17 +- .../inf101/v18/rogue101/game/RogueGame.java | 852 ++++++++ .../v18/rogue101/items/IConsumable.java | 9 - .../v18/rogue101/items/IMagicWeapon.java | 12 - .../v18/rogue101/items/IMeleeWeapon.java | 12 - .../v18/rogue101/items/IRangedWeapon.java | 12 - .../inf101/v18/rogue101/items/IStatic.java | 15 - .../inf101/v18/rogue101/items/IWeapon.java | 26 - .../rogue101/items/{ => buff}/Binoculars.java | 12 +- .../{IBuffItem.java => buff/BuffItem.java} | 21 +- .../v18/rogue101/items/{ => buff}/Shield.java | 14 +- .../rogue101/items/consumable/Consumable.java | 11 + .../items/{ => consumable}/HealthPotion.java | 19 +- .../items/{ => consumable}/Manga.java | 19 +- .../items/{ => container}/Backpack.java | 40 +- .../rogue101/items/{ => container}/Chest.java | 41 +- .../Container.java} | 16 +- .../v18/rogue101/items/container/Static.java | 20 + .../items/{Bow.java => weapon/BasicBow.java} | 12 +- .../{Sword.java => weapon/BasicSword.java} | 12 +- .../{Staff.java => weapon/FireStaff.java} | 12 +- .../rogue101/items/weapon/MagicWeapon.java | 15 + .../rogue101/items/weapon/MeleeWeapon.java | 15 + .../rogue101/items/weapon/RangedWeapon.java | 14 + .../v18/rogue101/items/weapon/Weapon.java | 29 + .../inf101/v18/rogue101/map/AGameMap.java | 446 +++++ .../java/inf101/v18/rogue101/map/GameMap.java | 417 +--- .../inf101/v18/rogue101/map/IGameMap.java | 48 - .../inf101/v18/rogue101/map/IMapView.java | 190 -- .../inf101/v18/rogue101/map/MapReader.java | 138 +- .../java/inf101/v18/rogue101/map/MapView.java | 177 ++ .../inf101/v18/rogue101/object/Actor.java | 43 + .../java/inf101/v18/rogue101/object/Dust.java | 49 + .../inf101/v18/rogue101/object/FakeWall.java | 31 + .../java/inf101/v18/rogue101/object/Item.java | 175 ++ .../rogue101/object/NonPlayerCharacter.java | 22 + .../rogue101/{objects => object}/Player.java | 251 +-- .../v18/rogue101/object/PlayerCharacter.java | 25 + .../{objects => object}/StairsDown.java | 17 +- .../{objects => object}/StairsUp.java | 17 +- .../FakeWall.java => object/Wall.java} | 21 +- .../inf101/v18/rogue101/objects/Dust.java | 49 - .../inf101/v18/rogue101/objects/IActor.java | 40 - .../inf101/v18/rogue101/objects/IItem.java | 181 -- .../v18/rogue101/objects/INonPlayer.java | 22 - .../inf101/v18/rogue101/objects/IPlayer.java | 26 - .../inf101/v18/rogue101/objects/Wall.java | 50 - .../java/inf101/v18/rogue101/shared/NPC.java | 226 --- .../inf101/v18/rogue101/state/AttackType.java | 12 + .../{states/Age.java => state/LifeStage.java} | 10 +- .../inf101/v18/rogue101/state/Occupation.java | 31 + .../v18/rogue101/state/Personality.java | 31 + .../java/inf101/v18/rogue101/state/Sound.java | 63 + .../inf101/v18/rogue101/states/Attack.java | 5 - .../v18/rogue101/states/Occupation.java | 16 - .../v18/rogue101/states/Personality.java | 16 - src/main/java/inf101/v18/util/Generator.java | 27 + src/main/java/inf101/v18/util/IGenerator.java | 53 - src/main/java/inf101/v18/util/NPCHelper.java | 245 +++ .../util/generators/AbstractGenerator.java | 36 +- .../v18/util/generators/AreaGenerator.java | 66 +- .../v18/util/generators/DoubleGenerator.java | 130 +- .../v18/util/generators/ElementGenerator.java | 52 +- .../generators/GridDirectionGenerator.java | 30 +- .../v18/util/generators/GridGenerator.java | 121 +- .../v18/util/generators/IntGenerator.java | 142 +- .../v18/util/generators/ListGenerator.java | 60 +- .../util/generators/LocationGenerator.java | 30 +- .../v18/util/generators/StringGenerator.java | 92 +- src/main/resources/audio/oh-no-113125.wav | Bin 0 -> 167544 bytes .../resources/fonts/README_ZXSpectrum-7.md | 22 +- .../fonts/README_lmmono10-regular.md | 175 +- src/test/java/AreaRetting.java | 199 +- src/test/java/GameMapTest.java | 182 +- src/test/java/GridRetting.java | 163 +- src/test/java/PlayerTest.java | 129 +- 118 files changed, 10314 insertions(+), 10155 deletions(-) delete mode 100644 src/main/java/inf101/v18/grid/ILocation.java create mode 100644 src/main/java/inf101/v18/grid/Location.java create mode 100644 src/main/java/inf101/v18/rogue101/enemies/Enemy.java delete mode 100644 src/main/java/inf101/v18/rogue101/examples/ExampleItem.java delete mode 100644 src/main/java/inf101/v18/rogue101/game/IGame.java create mode 100644 src/main/java/inf101/v18/rogue101/game/RogueGame.java delete mode 100644 src/main/java/inf101/v18/rogue101/items/IConsumable.java delete mode 100644 src/main/java/inf101/v18/rogue101/items/IMagicWeapon.java delete mode 100644 src/main/java/inf101/v18/rogue101/items/IMeleeWeapon.java delete mode 100644 src/main/java/inf101/v18/rogue101/items/IRangedWeapon.java delete mode 100644 src/main/java/inf101/v18/rogue101/items/IStatic.java delete mode 100644 src/main/java/inf101/v18/rogue101/items/IWeapon.java rename src/main/java/inf101/v18/rogue101/items/{ => buff}/Binoculars.java (70%) rename src/main/java/inf101/v18/rogue101/items/{IBuffItem.java => buff/BuffItem.java} (60%) rename src/main/java/inf101/v18/rogue101/items/{ => buff}/Shield.java (70%) create mode 100644 src/main/java/inf101/v18/rogue101/items/consumable/Consumable.java rename src/main/java/inf101/v18/rogue101/items/{ => consumable}/HealthPotion.java (63%) rename src/main/java/inf101/v18/rogue101/items/{ => consumable}/Manga.java (67%) rename src/main/java/inf101/v18/rogue101/items/{ => container}/Backpack.java (70%) rename src/main/java/inf101/v18/rogue101/items/{ => container}/Chest.java (67%) rename src/main/java/inf101/v18/rogue101/items/{IContainer.java => container/Container.java} (83%) create mode 100644 src/main/java/inf101/v18/rogue101/items/container/Static.java rename src/main/java/inf101/v18/rogue101/items/{Bow.java => weapon/BasicBow.java} (76%) rename src/main/java/inf101/v18/rogue101/items/{Sword.java => weapon/BasicSword.java} (76%) rename src/main/java/inf101/v18/rogue101/items/{Staff.java => weapon/FireStaff.java} (77%) create mode 100644 src/main/java/inf101/v18/rogue101/items/weapon/MagicWeapon.java create mode 100644 src/main/java/inf101/v18/rogue101/items/weapon/MeleeWeapon.java create mode 100644 src/main/java/inf101/v18/rogue101/items/weapon/RangedWeapon.java create mode 100644 src/main/java/inf101/v18/rogue101/items/weapon/Weapon.java create mode 100644 src/main/java/inf101/v18/rogue101/map/AGameMap.java delete mode 100644 src/main/java/inf101/v18/rogue101/map/IGameMap.java delete mode 100644 src/main/java/inf101/v18/rogue101/map/IMapView.java create mode 100644 src/main/java/inf101/v18/rogue101/map/MapView.java create mode 100644 src/main/java/inf101/v18/rogue101/object/Actor.java create mode 100644 src/main/java/inf101/v18/rogue101/object/Dust.java create mode 100644 src/main/java/inf101/v18/rogue101/object/FakeWall.java create mode 100644 src/main/java/inf101/v18/rogue101/object/Item.java create mode 100644 src/main/java/inf101/v18/rogue101/object/NonPlayerCharacter.java rename src/main/java/inf101/v18/rogue101/{objects => object}/Player.java (64%) create mode 100644 src/main/java/inf101/v18/rogue101/object/PlayerCharacter.java rename src/main/java/inf101/v18/rogue101/{objects => object}/StairsDown.java (58%) rename src/main/java/inf101/v18/rogue101/{objects => object}/StairsUp.java (58%) rename src/main/java/inf101/v18/rogue101/{objects/FakeWall.java => object/Wall.java} (66%) delete mode 100644 src/main/java/inf101/v18/rogue101/objects/Dust.java delete mode 100644 src/main/java/inf101/v18/rogue101/objects/IActor.java delete mode 100644 src/main/java/inf101/v18/rogue101/objects/IItem.java delete mode 100644 src/main/java/inf101/v18/rogue101/objects/INonPlayer.java delete mode 100644 src/main/java/inf101/v18/rogue101/objects/IPlayer.java delete mode 100644 src/main/java/inf101/v18/rogue101/objects/Wall.java delete mode 100644 src/main/java/inf101/v18/rogue101/shared/NPC.java create mode 100644 src/main/java/inf101/v18/rogue101/state/AttackType.java rename src/main/java/inf101/v18/rogue101/{states/Age.java => state/LifeStage.java} (56%) create mode 100644 src/main/java/inf101/v18/rogue101/state/Occupation.java create mode 100644 src/main/java/inf101/v18/rogue101/state/Personality.java create mode 100644 src/main/java/inf101/v18/rogue101/state/Sound.java delete mode 100644 src/main/java/inf101/v18/rogue101/states/Attack.java delete mode 100644 src/main/java/inf101/v18/rogue101/states/Occupation.java delete mode 100644 src/main/java/inf101/v18/rogue101/states/Personality.java create mode 100644 src/main/java/inf101/v18/util/Generator.java delete mode 100644 src/main/java/inf101/v18/util/IGenerator.java create mode 100644 src/main/java/inf101/v18/util/NPCHelper.java create mode 100644 src/main/resources/audio/oh-no-113125.wav diff --git a/README.md b/README.md index 049f01e..a3cbac2 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,3 @@ # Rogue101 + Originally a programming assignment in INF101. Added here for archival. \ No newline at end of file diff --git a/src/main/java/inf101/v18/gfx/IPaintLayer.java b/src/main/java/inf101/v18/gfx/IPaintLayer.java index 18a964a..eb7d2d9 100644 --- a/src/main/java/inf101/v18/gfx/IPaintLayer.java +++ b/src/main/java/inf101/v18/gfx/IPaintLayer.java @@ -1,38 +1,38 @@ package inf101.v18.gfx; public interface IPaintLayer { - /** - * Clear the layer. - * - *

- * Everything on the layer is removed, leaving only transparency. - */ - void clear(); + /** + * Clear the layer. + * + *

+ * Everything on the layer is removed, leaving only transparency. + */ + void clear(); - /** - * Send this layer to the back, so it will be drawn behind any other layers. - * - *

- * There will still be background behind this layer. You may clear it or draw to - * it using {@link Screen#clearBackground()}, - * {@link Screen#setBackground(javafx.scene.paint.Color)} and - * {@link Screen#getBackgroundContext()}. - */ - void layerToBack(); + /** + * Send this layer to the back, so it will be drawn behind any other layers. + * + *

+ * There will still be background behind this layer. You may clear it or draw to + * it using {@link Screen#clearBackground()}, + * {@link Screen#setBackground(javafx.scene.paint.Color)} and + * {@link Screen#getBackgroundContext()}. + */ + void layerToBack(); - /** - * Send this layer to the front, so it will be drawn on top of any other layers. - */ - void layerToFront(); + /** + * Send this layer to the front, so it will be drawn on top of any other layers. + */ + void layerToFront(); - /** - * @return Width (in pixels) of graphics layer - */ - double getWidth(); + /** + * @return Width (in pixels) of graphics layer + */ + double getWidth(); - /** - * @return Height (in pixels) of graphics layer - */ - double getHeight(); + /** + * @return Height (in pixels) of graphics layer + */ + double getHeight(); } diff --git a/src/main/java/inf101/v18/gfx/Screen.java b/src/main/java/inf101/v18/gfx/Screen.java index f20e7e2..ca2c011 100644 --- a/src/main/java/inf101/v18/gfx/Screen.java +++ b/src/main/java/inf101/v18/gfx/Screen.java @@ -1,12 +1,5 @@ package inf101.v18.gfx; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.IdentityHashMap; -import java.util.List; -import java.util.Map; -import java.util.function.Predicate; - import inf101.v18.gfx.gfxmode.TurtlePainter; import inf101.v18.gfx.textmode.Printer; import inf101.v18.rogue101.AppInfo; @@ -27,652 +20,698 @@ import javafx.stage.Stage; import javafx.stage.StageStyle; import javafx.stage.Window; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.IdentityHashMap; +import java.util.List; +import java.util.Map; +import java.util.function.Predicate; + public class Screen { - private static final double STD_CANVAS_WIDTH = 1280; - private static final List STD_ASPECTS = Arrays.asList(16.0 / 9.0, 16.0 / 10.0, 4.0 / 3.0); - /** 16:9 */ - public static final int ASPECT_WIDE = 0; - /** 16:10 */ - public static final int ASPECT_MEDIUM = 1; - /** 4:3 */ - public static final int ASPECT_CLASSIC = 2; - public static final int ASPECT_NATIVE = 2; - private static final int CONFIG_ASPECT_SHIFT = 0; - /** Screen's initial aspect ratio should be 16:9 */ - public static final int CONFIG_ASPECT_WIDE = 0 << CONFIG_ASPECT_SHIFT; - /** Screen's initial aspect ratio should be 16:10 */ - public static final int CONFIG_ASPECT_MEDIUM = 1 << CONFIG_ASPECT_SHIFT; - /** Screen's initial aspect ratio should be 4:3 */ - public static final int CONFIG_ASPECT_CLASSIC = 2 << CONFIG_ASPECT_SHIFT; - /** Screen's initial aspect ratio should be the same as the device display. */ - public static final int CONFIG_ASPECT_DEVICE = 3 << CONFIG_ASPECT_SHIFT; - private static final int CONFIG_ASPECT_MASK = 3 << CONFIG_ASPECT_SHIFT; + private static final double STD_CANVAS_WIDTH = 1280; + private static final List STD_ASPECTS = Arrays.asList(16.0 / 9.0, 16.0 / 10.0, 4.0 / 3.0); + /** + * 16:9 + */ + public static final int ASPECT_WIDE = 0; + /** + * 16:10 + */ + public static final int ASPECT_MEDIUM = 1; + /** + * 4:3 + */ + public static final int ASPECT_CLASSIC = 2; + public static final int ASPECT_NATIVE = 2; + private static final int CONFIG_ASPECT_SHIFT = 0; + /** + * Screen's initial aspect ratio should be 16:9 + */ + public static final int CONFIG_ASPECT_WIDE = 0 << CONFIG_ASPECT_SHIFT; + /** + * Screen's initial aspect ratio should be 16:10 + */ + public static final int CONFIG_ASPECT_MEDIUM = 1 << CONFIG_ASPECT_SHIFT; + /** + * Screen's initial aspect ratio should be 4:3 + */ + public static final int CONFIG_ASPECT_CLASSIC = 2 << CONFIG_ASPECT_SHIFT; + /** + * Screen's initial aspect ratio should be the same as the device display. + */ + public static final int CONFIG_ASPECT_DEVICE = 3 << CONFIG_ASPECT_SHIFT; + private static final int CONFIG_ASPECT_MASK = 3 << CONFIG_ASPECT_SHIFT; - private static final int CONFIG_SCREEN_SHIFT = 2; - /** Screen should start in a window. */ - public static final int CONFIG_SCREEN_WINDOWED = 0 << CONFIG_SCREEN_SHIFT; - /** Screen should start in a borderless window. */ - public static final int CONFIG_SCREEN_BORDERLESS = 1 << CONFIG_SCREEN_SHIFT; - /** Screen should start in a transparent window. */ - public static final int CONFIG_SCREEN_TRANSPARENT = 2 << CONFIG_SCREEN_SHIFT; - /** Screen should start fullscreen. */ - public static final int CONFIG_SCREEN_FULLSCREEN = 3 << CONFIG_SCREEN_SHIFT; - /** - * Screen should start fullscreen, without showing a "Press ESC to exit - * fullscreen" hint. - */ - public static final int CONFIG_SCREEN_FULLSCREEN_NO_HINT = 4 << CONFIG_SCREEN_SHIFT; - private static final int CONFIG_SCREEN_MASK = 7 << CONFIG_SCREEN_SHIFT; + private static final int CONFIG_SCREEN_SHIFT = 2; + /** + * Screen should start in a window. + */ + public static final int CONFIG_SCREEN_WINDOWED = 0 << CONFIG_SCREEN_SHIFT; + /** + * Screen should start in a borderless window. + */ + public static final int CONFIG_SCREEN_BORDERLESS = 1 << CONFIG_SCREEN_SHIFT; + /** + * Screen should start in a transparent window. + */ + public static final int CONFIG_SCREEN_TRANSPARENT = 2 << CONFIG_SCREEN_SHIFT; + /** + * Screen should start fullscreen. + */ + public static final int CONFIG_SCREEN_FULLSCREEN = 3 << CONFIG_SCREEN_SHIFT; + /** + * Screen should start fullscreen, without showing a "Press ESC to exit + * fullscreen" hint. + */ + public static final int CONFIG_SCREEN_FULLSCREEN_NO_HINT = 4 << CONFIG_SCREEN_SHIFT; + private static final int CONFIG_SCREEN_MASK = 7 << CONFIG_SCREEN_SHIFT; - private static final int CONFIG_PIXELS_SHIFT = 5; - /** - * Canvas size / number of pixels should be determined the default way. - * - * The default is {@link #CONFIG_PIXELS_DEVICE} for - * {@link #CONFIG_SCREEN_FULLSCREEN} and {@link #CONFIG_COORDS_DEVICE}, and - * {@link #CONFIG_PIXELS_STEP_SCALED} otherwise. - */ - public static final int CONFIG_PIXELS_DEFAULT = 0 << CONFIG_PIXELS_SHIFT; - /** - * Canvas size / number of pixels will be an integer multiple or fraction of the - * logical canvas size that fits the native display size. - * - * Scaling by whole integers makes it less likely that we get artifacts from - * rounding errors or JavaFX's antialiasing (e.g., fuzzy lines). - */ - public static final int CONFIG_PIXELS_STEP_SCALED = 1 << CONFIG_PIXELS_SHIFT; - /** Canvas size / number of pixels will the same as the native display size. */ - public static final int CONFIG_PIXELS_DEVICE = 2 << CONFIG_PIXELS_SHIFT; - /** - * Canvas size / number of pixels will the same as the logical canvas size - * (typically 1280x960). - */ - public static final int CONFIG_PIXELS_LOGICAL = 3 << CONFIG_PIXELS_SHIFT; - /** - * Canvas size / number of pixels will be scaled to fit the native display size. - */ - public static final int CONFIG_PIXELS_SCALED = 4 << CONFIG_PIXELS_SHIFT; - private static final int CONFIG_PIXELS_MASK = 7 << CONFIG_PIXELS_SHIFT; + private static final int CONFIG_PIXELS_SHIFT = 5; + /** + * Canvas size / number of pixels should be determined the default way. + *

+ * The default is {@link #CONFIG_PIXELS_DEVICE} for + * {@link #CONFIG_SCREEN_FULLSCREEN} and {@link #CONFIG_COORDS_DEVICE}, and + * {@link #CONFIG_PIXELS_STEP_SCALED} otherwise. + */ + public static final int CONFIG_PIXELS_DEFAULT = 0 << CONFIG_PIXELS_SHIFT; + /** + * Canvas size / number of pixels will be an integer multiple or fraction of the + * logical canvas size that fits the native display size. + *

+ * Scaling by whole integers makes it less likely that we get artifacts from + * rounding errors or JavaFX's antialiasing (e.g., fuzzy lines). + */ + public static final int CONFIG_PIXELS_STEP_SCALED = 1 << CONFIG_PIXELS_SHIFT; + /** + * Canvas size / number of pixels will the same as the native display size. + */ + public static final int CONFIG_PIXELS_DEVICE = 2 << CONFIG_PIXELS_SHIFT; + /** + * Canvas size / number of pixels will the same as the logical canvas size + * (typically 1280x960). + */ + public static final int CONFIG_PIXELS_LOGICAL = 3 << CONFIG_PIXELS_SHIFT; + /** + * Canvas size / number of pixels will be scaled to fit the native display size. + */ + public static final int CONFIG_PIXELS_SCALED = 4 << CONFIG_PIXELS_SHIFT; + private static final int CONFIG_PIXELS_MASK = 7 << CONFIG_PIXELS_SHIFT; - private static final int CONFIG_COORDS_SHIFT = 8; - /** - * The logical canvas coordinate system will be in logical units (i.e., 1280 - * pixels wide regardless of how many pixels wide the screen actually is) - */ - public static final int CONFIG_COORDS_LOGICAL = 0 << CONFIG_COORDS_SHIFT; - /** The logical canvas coordinate system will match the display. */ - public static final int CONFIG_COORDS_DEVICE = 1 << CONFIG_COORDS_SHIFT; - private static final int CONFIG_COORDS_MASK = 1 << CONFIG_COORDS_SHIFT; + private static final int CONFIG_COORDS_SHIFT = 8; + /** + * The logical canvas coordinate system will be in logical units (i.e., 1280 + * pixels wide regardless of how many pixels wide the screen actually is) + */ + public static final int CONFIG_COORDS_LOGICAL = 0 << CONFIG_COORDS_SHIFT; + /** + * The logical canvas coordinate system will match the display. + */ + public static final int CONFIG_COORDS_DEVICE = 1 << CONFIG_COORDS_SHIFT; + private static final int CONFIG_COORDS_MASK = 1 << CONFIG_COORDS_SHIFT; - private static final int CONFIG_FLAG_SHIFT = 9; - public static final int CONFIG_FLAG_HIDE_MOUSE = 1 << CONFIG_FLAG_SHIFT; - public static final int CONFIG_FLAG_NO_AUTOHIDE_MOUSE = 2 << CONFIG_FLAG_SHIFT; - public static final int CONFIG_FLAG_DEBUG = 4 << CONFIG_FLAG_SHIFT; - private static final int CONFIG_FLAG_MASK = 7; + private static final int CONFIG_FLAG_SHIFT = 9; + public static final int CONFIG_FLAG_HIDE_MOUSE = 1 << CONFIG_FLAG_SHIFT; + public static final int CONFIG_FLAG_NO_AUTOHIDE_MOUSE = 2 << CONFIG_FLAG_SHIFT; + public static final int CONFIG_FLAG_DEBUG = 4 << CONFIG_FLAG_SHIFT; + private static final int CONFIG_FLAG_MASK = 7; - /** - * Get the resolution of this screen, in DPI (pixels per inch). - * - * @return The primary display's DPI - * @see javafx.stage.Screen#getDpi() - */ - public static double getDisplayDpi() { - return javafx.stage.Screen.getPrimary().getDpi(); - } + /** + * Get the resolution of this screen, in DPI (pixels per inch). + * + * @return The primary display's DPI + * @see javafx.stage.Screen#getDpi() + */ + public static double getDisplayDpi() { + return javafx.stage.Screen.getPrimary().getDpi(); + } - /** - * Get the height of the display, in pixels. - * - *

- * This takes into account such things as toolbars, menus and such (on a - * desktop), and pixel density (e.g., on high resolution mobile devices). - * - * @return Height of the display - * @see javafx.stage.Screen#getVisualBounds() - */ - public static double getDisplayHeight() { - return javafx.stage.Screen.getPrimary().getVisualBounds().getHeight(); - } + /** + * Get the height of the display, in pixels. + * + *

+ * This takes into account such things as toolbars, menus and such (on a + * desktop), and pixel density (e.g., on high resolution mobile devices). + * + * @return Height of the display + * @see javafx.stage.Screen#getVisualBounds() + */ + public static double getDisplayHeight() { + return javafx.stage.Screen.getPrimary().getVisualBounds().getHeight(); + } - /** - * Get the width of the display, in pixels. - * - *

- * This takes into account such things as toolbars, menus and such (on a - * desktop), and pixel density (e.g., on high resolution mobile devices). - * - * @return Width of the display - * @see javafx.stage.Screen#getVisualBounds() - */ - public static double getDisplayWidth() { - return javafx.stage.Screen.getPrimary().getVisualBounds().getWidth(); - } + /** + * Get the width of the display, in pixels. + * + *

+ * This takes into account such things as toolbars, menus and such (on a + * desktop), and pixel density (e.g., on high resolution mobile devices). + * + * @return Width of the display + * @see javafx.stage.Screen#getVisualBounds() + */ + public static double getDisplayWidth() { + return javafx.stage.Screen.getPrimary().getVisualBounds().getWidth(); + } - /** - * Get the native physical height of the screen, in pixels. - * - *

- * This will not include such things as toolbars, menus and such (on a desktop), - * or take pixel density into account (e.g., on high resolution mobile devices). - * - * @return Raw width of the display - * @see javafx.stage.Screen#getBounds() - */ - public static double getRawDisplayHeight() { - return javafx.stage.Screen.getPrimary().getBounds().getHeight(); - } + /** + * Get the native physical height of the screen, in pixels. + * + *

+ * This will not include such things as toolbars, menus and such (on a desktop), + * or take pixel density into account (e.g., on high resolution mobile devices). + * + * @return Raw width of the display + * @see javafx.stage.Screen#getBounds() + */ + public static double getRawDisplayHeight() { + return javafx.stage.Screen.getPrimary().getBounds().getHeight(); + } - /** - * Get the native physical width of the screen, in pixels. - * - *

- * This will not include such things as toolbars, menus and such (on a desktop), - * or take pixel density into account (e.g., on high resolution mobile devices). - * - * @return Raw width of the display - * @see javafx.stage.Screen#getBounds() - */ - public static double getRawDisplayWidth() { - return javafx.stage.Screen.getPrimary().getBounds().getWidth(); - } + /** + * Get the native physical width of the screen, in pixels. + * + *

+ * This will not include such things as toolbars, menus and such (on a desktop), + * or take pixel density into account (e.g., on high resolution mobile devices). + * + * @return Raw width of the display + * @see javafx.stage.Screen#getBounds() + */ + public static double getRawDisplayWidth() { + return javafx.stage.Screen.getPrimary().getBounds().getWidth(); + } - /** - * Start the paint display system. - * - * This will open a window on the screen, and set up background, text and paint - * layers, and listener to handle keyboard input. - * - * @param stage - * A JavaFX {@link javafx.stage.Stage}, typically obtained from the - * {@link javafx.application.Application#start(Stage)} method - * @return A screen for drawing on - */ - public static Screen startPaintScene(Stage stage) { - return startPaintScene(stage, CONFIG_SCREEN_FULLSCREEN_NO_HINT); - } + /** + * Start the paint display system. + *

+ * This will open a window on the screen, and set up background, text and paint + * layers, and listener to handle keyboard input. + * + * @param stage A JavaFX {@link javafx.stage.Stage}, typically obtained from the + * {@link javafx.application.Application#start(Stage)} method + * @return A screen for drawing on + */ + public static Screen startPaintScene(Stage stage) { + return startPaintScene(stage, CONFIG_SCREEN_FULLSCREEN_NO_HINT); + } - /** - * Start the paint display system. - * - * This will open a window on the screen, and set up background, text and paint - * layers, and listener to handle keyboard input. - * - * @param stage - * A JavaFX {@link javafx.stage.Stage}, typically obtained from the - * {@link javafx.application.Application#start(Stage)} method - * @return A screen for drawing on - */ - public static Screen startPaintScene(Stage stage, int configuration) { - int configAspect = (configuration & CONFIG_ASPECT_MASK); - int configScreen = (configuration & CONFIG_SCREEN_MASK); - int configPixels = (configuration & CONFIG_PIXELS_MASK); - int configCoords = (configuration & CONFIG_COORDS_MASK); - int configFlags = (configuration & CONFIG_FLAG_MASK); - boolean debug = (configFlags & CONFIG_FLAG_DEBUG) != 0; - if (configPixels == CONFIG_PIXELS_DEFAULT) { - if (configCoords == CONFIG_COORDS_DEVICE || configScreen == CONFIG_SCREEN_FULLSCREEN) - configPixels = CONFIG_PIXELS_DEVICE; - else - configPixels = CONFIG_PIXELS_STEP_SCALED; - } - double rawWidth = getRawDisplayWidth(); - double rawHeight = getRawDisplayHeight(); - double width = getDisplayWidth() - 40; - double height = getDisplayHeight() - 100; - double canvasAspect = configAspect == CONFIG_ASPECT_DEVICE ? rawWidth / rawHeight - : STD_ASPECTS.get(configAspect); - double xScale = (height * canvasAspect) / Screen.STD_CANVAS_WIDTH; - double yScale = (width / canvasAspect) / (Screen.STD_CANVAS_WIDTH / canvasAspect); - double scale = Math.min(xScale, yScale); - if (configPixels == CONFIG_PIXELS_STEP_SCALED) { - if (scale > 1.0) - scale = Math.max(1, Math.floor(scale)); - else if (scale < 1.0) - scale = 1 / Math.max(1, Math.floor(1 / scale)); - } - double winWidth = Math.floor(Screen.STD_CANVAS_WIDTH * scale); - double winHeight = Math.floor((Screen.STD_CANVAS_WIDTH / canvasAspect) * scale); - double canvasWidth = Screen.STD_CANVAS_WIDTH; - double canvasHeight = Math.floor(3 * Screen.STD_CANVAS_WIDTH / 4); - double pixWidth = canvasWidth; - double pixHeight = canvasHeight; - if (configPixels == CONFIG_PIXELS_SCALED || configPixels == CONFIG_PIXELS_STEP_SCALED) { - pixWidth *= scale; - pixHeight *= scale; - } else if (configPixels == CONFIG_PIXELS_DEVICE) { - pixWidth = rawWidth; - pixHeight = rawHeight; - } - if (configCoords == CONFIG_COORDS_DEVICE) { - canvasWidth = pixWidth; - canvasHeight = pixHeight; - } - if (debug) { - System.out.printf("Screen setup:%n"); - System.out.printf(" Display: %.0fx%.0f (raw %.0fx%.0f)%n", width, height, rawWidth, rawHeight); - System.out.printf(" Window: %.0fx%.0f%n", winWidth, winHeight); - System.out.printf(" Canvas: physical %.0fx%.0f, logical %.0fx%.0f%n", pixWidth, pixHeight, canvasWidth, - canvasHeight); - System.out.printf(" Aspect: %.5f Scale: %.5f%n", canvasAspect, scale); - } - Group root = new Group(); - Scene scene = new Scene(root, winWidth, winHeight, Color.BLACK); - stage.setScene(scene); - stage.setTitle(AppInfo.APP_NAME); - if ((configFlags & CONFIG_FLAG_HIDE_MOUSE) != 0) { - scene.setCursor(Cursor.NONE); - } + /** + * Start the paint display system. + *

+ * This will open a window on the screen, and set up background, text and paint + * layers, and listener to handle keyboard input. + * + * @param stage A JavaFX {@link javafx.stage.Stage}, typically obtained from the + * {@link javafx.application.Application#start(Stage)} method + * @return A screen for drawing on + */ + public static Screen startPaintScene(Stage stage, int configuration) { + int configAspect = (configuration & CONFIG_ASPECT_MASK); + int configScreen = (configuration & CONFIG_SCREEN_MASK); + int configPixels = (configuration & CONFIG_PIXELS_MASK); + int configCoords = (configuration & CONFIG_COORDS_MASK); + int configFlags = (configuration & CONFIG_FLAG_MASK); + boolean debug = (configFlags & CONFIG_FLAG_DEBUG) != 0; + if (configPixels == CONFIG_PIXELS_DEFAULT) { + if (configCoords == CONFIG_COORDS_DEVICE || configScreen == CONFIG_SCREEN_FULLSCREEN) { + configPixels = CONFIG_PIXELS_DEVICE; + } else { + configPixels = CONFIG_PIXELS_STEP_SCALED; + } + } + double rawWidth = getRawDisplayWidth(); + double rawHeight = getRawDisplayHeight(); + double width = getDisplayWidth() - 40; + double height = getDisplayHeight() - 100; + double canvasAspect = configAspect == CONFIG_ASPECT_DEVICE ? rawWidth / rawHeight + : STD_ASPECTS.get(configAspect); + double xScale = (height * canvasAspect) / Screen.STD_CANVAS_WIDTH; + double yScale = (width / canvasAspect) / (Screen.STD_CANVAS_WIDTH / canvasAspect); + double scale = Math.min(xScale, yScale); + if (configPixels == CONFIG_PIXELS_STEP_SCALED) { + if (scale > 1.0) { + scale = Math.max(1, Math.floor(scale)); + } else if (scale < 1.0) { + scale = 1 / Math.max(1, Math.floor(1 / scale)); + } + } + double winWidth = Math.floor(Screen.STD_CANVAS_WIDTH * scale); + double winHeight = Math.floor((Screen.STD_CANVAS_WIDTH / canvasAspect) * scale); + double canvasWidth = Screen.STD_CANVAS_WIDTH; + double canvasHeight = Math.floor(3 * Screen.STD_CANVAS_WIDTH / 4); + double pixWidth = canvasWidth; + double pixHeight = canvasHeight; + if (configPixels == CONFIG_PIXELS_SCALED || configPixels == CONFIG_PIXELS_STEP_SCALED) { + pixWidth *= scale; + pixHeight *= scale; + } else if (configPixels == CONFIG_PIXELS_DEVICE) { + pixWidth = rawWidth; + pixHeight = rawHeight; + } + if (configCoords == CONFIG_COORDS_DEVICE) { + canvasWidth = pixWidth; + canvasHeight = pixHeight; + } + if (debug) { + System.out.printf("Screen setup:%n"); + System.out.printf(" Display: %.0fx%.0f (raw %.0fx%.0f)%n", width, height, rawWidth, rawHeight); + System.out.printf(" Window: %.0fx%.0f%n", winWidth, winHeight); + System.out.printf(" Canvas: physical %.0fx%.0f, logical %.0fx%.0f%n", pixWidth, pixHeight, canvasWidth, + canvasHeight); + System.out.printf(" Aspect: %.5f Scale: %.5f%n", canvasAspect, scale); + } + Group root = new Group(); + Scene scene = new Scene(root, winWidth, winHeight, Color.BLACK); + stage.setScene(scene); + stage.setTitle(AppInfo.APP_NAME); + if ((configFlags & CONFIG_FLAG_HIDE_MOUSE) != 0) { + scene.setCursor(Cursor.NONE); + } - Screen pScene = new Screen(scene.getWidth(), scene.getHeight(), // - pixWidth, pixHeight, // - canvasWidth, canvasHeight); - pScene.subScene.widthProperty().bind(scene.widthProperty()); - pScene.subScene.heightProperty().bind(scene.heightProperty()); - pScene.debug = debug; - pScene.hideFullScreenMouseCursor = (configFlags & CONFIG_FLAG_NO_AUTOHIDE_MOUSE) == 0; - root.getChildren().add(pScene.subScene); + Screen pScene = new Screen(scene.getWidth(), scene.getHeight(), // + pixWidth, pixHeight, // + canvasWidth, canvasHeight); + pScene.subScene.widthProperty().bind(scene.widthProperty()); + pScene.subScene.heightProperty().bind(scene.heightProperty()); + pScene.debug = debug; + pScene.hideFullScreenMouseCursor = (configFlags & CONFIG_FLAG_NO_AUTOHIDE_MOUSE) == 0; + root.getChildren().add(pScene.subScene); - boolean[] suppressKeyTyped = { false }; + boolean[] suppressKeyTyped = {false}; - switch (configScreen) { - case CONFIG_SCREEN_WINDOWED: - break; - case CONFIG_SCREEN_BORDERLESS: - stage.initStyle(StageStyle.UNDECORATED); - break; - case CONFIG_SCREEN_TRANSPARENT: - stage.initStyle(StageStyle.TRANSPARENT); - break; - case CONFIG_SCREEN_FULLSCREEN_NO_HINT: - stage.setFullScreenExitHint(""); - // fall-through - case CONFIG_SCREEN_FULLSCREEN: - stage.setFullScreen(true); - break; - } - scene.setOnKeyPressed((KeyEvent event) -> { - if (!event.isConsumed() && pScene.keyOverride != null && pScene.keyOverride.test(event)) { - event.consume(); - } - if (!event.isConsumed() && pScene.minimalKeyHandler(event)) { - event.consume(); - } - if (!event.isConsumed() && pScene.keyPressedHandler != null && pScene.keyPressedHandler.test(event)) { - event.consume(); - } - if (pScene.logKeyEvents) - System.err.println(event); - suppressKeyTyped[0] = event.isConsumed(); - }); - scene.setOnKeyTyped((KeyEvent event) -> { - if (suppressKeyTyped[0]) { - suppressKeyTyped[0] = false; - event.consume(); - } - if (!event.isConsumed() && pScene.keyTypedHandler != null && pScene.keyTypedHandler.test(event)) { - event.consume(); - } - if (pScene.logKeyEvents) - System.err.println(event); - }); - scene.setOnKeyReleased((KeyEvent event) -> { - suppressKeyTyped[0] = false; - if (!event.isConsumed() && pScene.keyReleasedHandler != null && pScene.keyReleasedHandler.test(event)) { - event.consume(); - } - if (pScene.logKeyEvents) - System.err.println(event); - }); - return pScene; - } + switch (configScreen) { + case CONFIG_SCREEN_WINDOWED: + break; + case CONFIG_SCREEN_BORDERLESS: + stage.initStyle(StageStyle.UNDECORATED); + break; + case CONFIG_SCREEN_TRANSPARENT: + stage.initStyle(StageStyle.TRANSPARENT); + break; + case CONFIG_SCREEN_FULLSCREEN_NO_HINT: + stage.setFullScreenExitHint(""); + // fall-through + case CONFIG_SCREEN_FULLSCREEN: + stage.setFullScreen(true); + break; + } + scene.setOnKeyPressed((KeyEvent event) -> { + if (!event.isConsumed() && pScene.keyOverride != null && pScene.keyOverride.test(event)) { + event.consume(); + } + if (!event.isConsumed() && pScene.minimalKeyHandler(event)) { + event.consume(); + } + if (!event.isConsumed() && pScene.keyPressedHandler != null && pScene.keyPressedHandler.test(event)) { + event.consume(); + } + if (pScene.logKeyEvents) { + System.err.println(event); + } + suppressKeyTyped[0] = event.isConsumed(); + }); + scene.setOnKeyTyped((KeyEvent event) -> { + if (suppressKeyTyped[0]) { + suppressKeyTyped[0] = false; + event.consume(); + } + if (!event.isConsumed() && pScene.keyTypedHandler != null && pScene.keyTypedHandler.test(event)) { + event.consume(); + } + if (pScene.logKeyEvents) { + System.err.println(event); + } + }); + scene.setOnKeyReleased((KeyEvent event) -> { + suppressKeyTyped[0] = false; + if (!event.isConsumed() && pScene.keyReleasedHandler != null && pScene.keyReleasedHandler.test(event)) { + event.consume(); + } + if (pScene.logKeyEvents) { + System.err.println(event); + } + }); + return pScene; + } - private final double rawCanvasWidth; - private final double rawCanvasHeight; - private boolean logKeyEvents = false; - private final SubScene subScene; - private final List canvases = new ArrayList<>(); - private final Map layerCanvases = new IdentityHashMap<>(); - private final Canvas background; - private final Group root; - //private Paint bgColor = Color.CORNFLOWERBLUE; - private Paint bgColor = Color.BLACK; - private int aspect = 0; - private double scaling = 0; - private double currentScale = 1.0; - private double currentFit = 1.0; - private double resolutionScale = 1.0; - private int maxScale = 1; - private Predicate keyOverride = null; + private final double rawCanvasWidth; + private final double rawCanvasHeight; + private boolean logKeyEvents = false; + private final SubScene subScene; + private final List canvases = new ArrayList<>(); + private final Map layerCanvases = new IdentityHashMap<>(); + private final Canvas background; + private final Group root; + //private Paint bgColor = Color.CORNFLOWERBLUE; + private Paint bgColor = Color.BLACK; + private int aspect = 0; + private double scaling = 0; + private double currentScale = 1.0; + private double currentFit = 1.0; + private double resolutionScale = 1.0; + private int maxScale = 1; + private Predicate keyOverride = null; - private Predicate keyPressedHandler = null; + private Predicate keyPressedHandler = null; - private Predicate keyTypedHandler = null; + private Predicate keyTypedHandler = null; - private Predicate keyReleasedHandler = null; + private Predicate keyReleasedHandler = null; - private boolean debug = true; + private boolean debug = true; - private List aspects; + private List aspects; - private boolean hideFullScreenMouseCursor = true; + private boolean hideFullScreenMouseCursor = true; - private Cursor oldCursor; + private Cursor oldCursor; - public Screen(double width, double height, double pixWidth, double pixHeight, double canvasWidth, - double canvasHeight) { - root = new Group(); - subScene = new SubScene(root, Math.floor(width), Math.floor(height)); - resolutionScale = pixWidth / canvasWidth; - this.rawCanvasWidth = Math.floor(pixWidth); - this.rawCanvasHeight = Math.floor(pixHeight); - double aspectRatio = width / height; - aspect = 0; - for (double a : STD_ASPECTS) - if (Math.abs(aspectRatio - a) < 0.01) { - break; - } else { - aspect++; - } - aspects = new ArrayList<>(STD_ASPECTS); - if (aspect >= STD_ASPECTS.size()) { - aspects.add(aspectRatio); - } - background = new Canvas(rawCanvasWidth, rawCanvasHeight); - background.getGraphicsContext2D().scale(resolutionScale, resolutionScale); - setBackground(bgColor); - clearBackground(); - root.getChildren().add(background); - subScene.layoutBoundsProperty() - .addListener((ObservableValue observable, Bounds oldBounds, Bounds bounds) -> { - recomputeLayout(false); - }); - } + public Screen(double width, double height, double pixWidth, double pixHeight, double canvasWidth, + double canvasHeight) { + root = new Group(); + subScene = new SubScene(root, Math.floor(width), Math.floor(height)); + resolutionScale = pixWidth / canvasWidth; + this.rawCanvasWidth = Math.floor(pixWidth); + this.rawCanvasHeight = Math.floor(pixHeight); + double aspectRatio = width / height; + aspect = 0; + for (double a : STD_ASPECTS) + if (Math.abs(aspectRatio - a) < 0.01) { + break; + } else { + aspect++; + } + aspects = new ArrayList<>(STD_ASPECTS); + if (aspect >= STD_ASPECTS.size()) { + aspects.add(aspectRatio); + } + background = new Canvas(rawCanvasWidth, rawCanvasHeight); + background.getGraphicsContext2D().scale(resolutionScale, resolutionScale); + setBackground(bgColor); + clearBackground(); + root.getChildren().add(background); + subScene.layoutBoundsProperty() + .addListener((ObservableValue observable, Bounds oldBounds, Bounds bounds) -> { + recomputeLayout(false); + }); + } - public void clearBackground() { - getBackgroundContext().setFill(bgColor); - getBackgroundContext().fillRect(0.0, 0.0, background.getWidth(), background.getHeight()); - } + public void clearBackground() { + getBackgroundContext().setFill(bgColor); + getBackgroundContext().fillRect(0.0, 0.0, background.getWidth(), background.getHeight()); + } - public TurtlePainter createPainter() { - Canvas canvas = new Canvas(rawCanvasWidth, rawCanvasHeight); - canvas.getGraphicsContext2D().scale(resolutionScale, resolutionScale); - canvases.add(canvas); - root.getChildren().add(canvas); - return new TurtlePainter(this, canvas); - } + public TurtlePainter createPainter() { + Canvas canvas = new Canvas(rawCanvasWidth, rawCanvasHeight); + canvas.getGraphicsContext2D().scale(resolutionScale, resolutionScale); + canvases.add(canvas); + root.getChildren().add(canvas); + return new TurtlePainter(this, canvas); + } - public Printer createPrinter() { - Canvas canvas = new Canvas(rawCanvasWidth, rawCanvasHeight); - canvas.getGraphicsContext2D().scale(resolutionScale, resolutionScale); - canvases.add(canvas); - root.getChildren().add(canvas); - return new Printer(this, canvas); - } + public Printer createPrinter() { + Canvas canvas = new Canvas(rawCanvasWidth, rawCanvasHeight); + canvas.getGraphicsContext2D().scale(resolutionScale, resolutionScale); + canvases.add(canvas); + root.getChildren().add(canvas); + return new Printer(this, canvas); + } - public void cycleAspect() { - aspect = (aspect + 1) % aspects.size(); - recomputeLayout(false); - } + public void cycleAspect() { + aspect = (aspect + 1) % aspects.size(); + recomputeLayout(false); + } - public void fitScaling() { - scaling = 0; - recomputeLayout(true); - } + public void fitScaling() { + scaling = 0; + recomputeLayout(true); + } - public int getAspect() { - return aspect; - } + public int getAspect() { + return aspect; + } - public GraphicsContext getBackgroundContext() { - return background.getGraphicsContext2D(); - } + public GraphicsContext getBackgroundContext() { + return background.getGraphicsContext2D(); + } - public double getHeight() { - return Math.floor(getRawHeight() / resolutionScale); - } + public double getHeight() { + return Math.floor(getRawHeight() / resolutionScale); + } - /** @return the keyOverride */ - public Predicate getKeyOverride() { - return keyOverride; - } + /** + * @return the keyOverride + */ + public Predicate getKeyOverride() { + return keyOverride; + } - /** @return the keyHandler */ - public Predicate getKeyPressedHandler() { - return keyPressedHandler; - } + /** + * @return the keyHandler + */ + public Predicate getKeyPressedHandler() { + return keyPressedHandler; + } - /** @return the keyReleasedHandler */ - public Predicate getKeyReleasedHandler() { - return keyReleasedHandler; - } + /** + * @return the keyReleasedHandler + */ + public Predicate getKeyReleasedHandler() { + return keyReleasedHandler; + } - /** @return the keyTypedHandler */ - public Predicate getKeyTypedHandler() { - return keyTypedHandler; - } + /** + * @return the keyTypedHandler + */ + public Predicate getKeyTypedHandler() { + return keyTypedHandler; + } - public double getRawHeight() { - return Math.floor(rawCanvasWidth / aspects.get(aspect)); - } + public double getRawHeight() { + return Math.floor(rawCanvasWidth / aspects.get(aspect)); + } - public double getRawWidth() { - return rawCanvasWidth; - } + public double getRawWidth() { + return rawCanvasWidth; + } - public double getWidth() { - return Math.floor(getRawWidth() / resolutionScale); - } + public double getWidth() { + return Math.floor(getRawWidth() / resolutionScale); + } - public void hideMouseCursor() { - subScene.getScene().setCursor(Cursor.NONE); - } + public void hideMouseCursor() { + subScene.getScene().setCursor(Cursor.NONE); + } - public boolean isFullScreen() { - Window window = subScene.getScene().getWindow(); - if (window instanceof Stage) - return ((Stage) window).isFullScreen(); - else - return false; - } + public boolean isFullScreen() { + Window window = subScene.getScene().getWindow(); + if (window instanceof Stage) { + return ((Stage) window).isFullScreen(); + } else { + return false; + } + } - public boolean minimalKeyHandler(KeyEvent event) { - KeyCode code = event.getCode(); - if (event.isShortcutDown()) { - if (code == KeyCode.Q) { - System.exit(0); - } else if (code == KeyCode.PLUS) { - zoomIn(); - return true; - } else if (code == KeyCode.MINUS) { - zoomOut(); - return true; - } - } else if (!(event.isAltDown() || event.isControlDown() || event.isMetaDown() || event.isShiftDown())) { - if (code == KeyCode.F11) { - setFullScreen(!isFullScreen()); - return true; - } - } + public boolean minimalKeyHandler(KeyEvent event) { + KeyCode code = event.getCode(); + if (event.isShortcutDown()) { + if (code == KeyCode.Q) { + System.exit(0); + } else if (code == KeyCode.PLUS) { + zoomIn(); + return true; + } else if (code == KeyCode.MINUS) { + zoomOut(); + return true; + } + } else if (!(event.isAltDown() || event.isControlDown() || event.isMetaDown() || event.isShiftDown())) { + if (code == KeyCode.F11) { + setFullScreen(!isFullScreen()); + return true; + } + } - return false; - } + return false; + } - public void moveToBack(IPaintLayer layer) { - Canvas canvas = layerCanvases.get(layer); - if (canvas != null) { - canvas.toBack(); - background.toBack(); - } - } + public void moveToBack(IPaintLayer layer) { + Canvas canvas = layerCanvases.get(layer); + if (canvas != null) { + canvas.toBack(); + background.toBack(); + } + } - public void moveToFront(IPaintLayer layer) { - Canvas canvas = layerCanvases.get(layer); - if (canvas != null) { - canvas.toFront(); - } - } + public void moveToFront(IPaintLayer layer) { + Canvas canvas = layerCanvases.get(layer); + if (canvas != null) { + canvas.toFront(); + } + } - private void recomputeLayout(boolean resizeWindow) { - double xScale = subScene.getWidth() / getRawWidth(); - double yScale = subScene.getHeight() / getRawHeight(); - double xMaxScale = getDisplayWidth() / getRawWidth(); - double yMaxScale = getDisplayHeight() / getRawHeight(); - currentFit = Math.min(xScale, yScale); - maxScale = (int) Math.max(1, Math.ceil(Math.min(xMaxScale, yMaxScale))); - currentScale = scaling == 0 ? currentFit : scaling; + private void recomputeLayout(boolean resizeWindow) { + double xScale = subScene.getWidth() / getRawWidth(); + double yScale = subScene.getHeight() / getRawHeight(); + double xMaxScale = getDisplayWidth() / getRawWidth(); + double yMaxScale = getDisplayHeight() / getRawHeight(); + currentFit = Math.min(xScale, yScale); + maxScale = (int) Math.max(1, Math.ceil(Math.min(xMaxScale, yMaxScale))); + currentScale = scaling == 0 ? currentFit : scaling; - if (resizeWindow) { - Scene scene = subScene.getScene(); - Window window = scene.getWindow(); - double hBorder = window.getWidth() - scene.getWidth(); - double vBorder = window.getHeight() - scene.getHeight(); - double myWidth = getRawWidth() * currentScale; - double myHeight = getRawHeight() * currentScale; - if (debug) - System.out.printf( - "Resizing before: screen: %1.0fx%1.0f, screen: %1.0fx%1.0f, scene: %1.0fx%1.0f, window: %1.0fx%1.0f,%n border: %1.0fx%1.0f, new window size: %1.0fx%1.0f, canvas size: %1.0fx%1.0f%n", // - javafx.stage.Screen.getPrimary().getVisualBounds().getWidth(), - javafx.stage.Screen.getPrimary().getVisualBounds().getHeight(), subScene.getWidth(), - subScene.getHeight(), scene.getWidth(), scene.getHeight(), window.getWidth(), - window.getHeight(), hBorder, vBorder, myWidth, myHeight, getRawWidth(), getRawHeight()); - // this.setWidth(myWidth); - // this.setHeight(myHeight); - window.setWidth(myWidth + hBorder); - window.setHeight(myHeight + vBorder); - if (debug) - System.out.printf( - "Resizing after : screen: %1.0fx%1.0f, screen: %1.0fx%1.0f, scene: %1.0fx%1.0f, window: %1.0fx%1.0f,%n border: %1.0fx%1.0f, new window size: %1.0fx%1.0f, canvas size: %1.0fx%1.0f%n", - javafx.stage.Screen.getPrimary().getVisualBounds().getWidth(), - javafx.stage.Screen.getPrimary().getVisualBounds().getHeight(), subScene.getWidth(), - subScene.getHeight(), scene.getWidth(), scene.getHeight(), window.getWidth(), - window.getHeight(), hBorder, vBorder, myWidth, myHeight, getRawWidth(), getRawHeight()); - } + if (resizeWindow) { + Scene scene = subScene.getScene(); + Window window = scene.getWindow(); + double hBorder = window.getWidth() - scene.getWidth(); + double vBorder = window.getHeight() - scene.getHeight(); + double myWidth = getRawWidth() * currentScale; + double myHeight = getRawHeight() * currentScale; + if (debug) { + System.out.printf( + "Resizing before: screen: %1.0fx%1.0f, screen: %1.0fx%1.0f, scene: %1.0fx%1.0f, window: %1.0fx%1.0f,%n border: %1.0fx%1.0f, new window size: %1.0fx%1.0f, canvas size: %1.0fx%1.0f%n", // + javafx.stage.Screen.getPrimary().getVisualBounds().getWidth(), + javafx.stage.Screen.getPrimary().getVisualBounds().getHeight(), subScene.getWidth(), + subScene.getHeight(), scene.getWidth(), scene.getHeight(), window.getWidth(), + window.getHeight(), hBorder, vBorder, myWidth, myHeight, getRawWidth(), getRawHeight()); + } + // this.setWidth(myWidth); + // this.setHeight(myHeight); + window.setWidth(myWidth + hBorder); + window.setHeight(myHeight + vBorder); + if (debug) { + System.out.printf( + "Resizing after : screen: %1.0fx%1.0f, screen: %1.0fx%1.0f, scene: %1.0fx%1.0f, window: %1.0fx%1.0f,%n border: %1.0fx%1.0f, new window size: %1.0fx%1.0f, canvas size: %1.0fx%1.0f%n", + javafx.stage.Screen.getPrimary().getVisualBounds().getWidth(), + javafx.stage.Screen.getPrimary().getVisualBounds().getHeight(), subScene.getWidth(), + subScene.getHeight(), scene.getWidth(), scene.getHeight(), window.getWidth(), + window.getHeight(), hBorder, vBorder, myWidth, myHeight, getRawWidth(), getRawHeight()); + } + } - if (debug) - System.out.printf("Rescaling: subscene %1.2fx%1.2f, scale %1.2f, aspect %.4f (%d), canvas %1.0fx%1.0f%n", - subScene.getWidth(), subScene.getHeight(), currentScale, aspects.get(aspect), aspect, getRawWidth(), - getRawHeight()); - for (Node n : root.getChildren()) { - n.relocate(Math.floor(subScene.getWidth() / 2), - Math.floor(subScene.getHeight() / 2 + (rawCanvasHeight - getRawHeight()) * currentScale / 2)); - n.setTranslateX(-Math.floor(rawCanvasWidth / 2)); - n.setTranslateY(-Math.floor(rawCanvasHeight / 2)); - if (debug) - System.out.printf(" * layout %1.2fx%1.2f, translate %1.2fx%1.2f%n", n.getLayoutX(), n.getLayoutY(), - n.getTranslateX(), n.getTranslateY()); - n.setScaleX(currentScale); - n.setScaleY(currentScale); - } - } + if (debug) { + System.out.printf("Rescaling: subscene %1.2fx%1.2f, scale %1.2f, aspect %.4f (%d), canvas %1.0fx%1.0f%n", + subScene.getWidth(), subScene.getHeight(), currentScale, aspects.get(aspect), aspect, getRawWidth(), + getRawHeight()); + } + for (Node n : root.getChildren()) { + n.relocate(Math.floor(subScene.getWidth() / 2), + Math.floor(subScene.getHeight() / 2 + (rawCanvasHeight - getRawHeight()) * currentScale / 2)); + n.setTranslateX(-Math.floor(rawCanvasWidth / 2)); + n.setTranslateY(-Math.floor(rawCanvasHeight / 2)); + if (debug) { + System.out.printf(" * layout %1.2fx%1.2f, translate %1.2fx%1.2f%n", n.getLayoutX(), n.getLayoutY(), + n.getTranslateX(), n.getTranslateY()); + } + n.setScaleX(currentScale); + n.setScaleY(currentScale); + } + } - public void setAspect(int aspect) { - this.aspect = (aspect) % aspects.size(); - recomputeLayout(false); - } + public void setAspect(int aspect) { + this.aspect = (aspect) % aspects.size(); + recomputeLayout(false); + } - public void setBackground(Paint bgColor) { - this.bgColor = bgColor; - subScene.setFill(bgColor instanceof Color ? ((Color) bgColor).darker() : bgColor); - } + public void setBackground(Paint bgColor) { + this.bgColor = bgColor; + subScene.setFill(bgColor instanceof Color ? ((Color) bgColor).darker() : bgColor); + } - public void setFullScreen(boolean fullScreen) { - Window window = subScene.getScene().getWindow(); - if (window instanceof Stage) { - ((Stage) window).setFullScreenExitHint(""); - ((Stage) window).setFullScreen(fullScreen); - if (hideFullScreenMouseCursor) { - if (fullScreen) { - oldCursor = subScene.getScene().getCursor(); - subScene.getScene().setCursor(Cursor.NONE); - } else if (oldCursor != null) { - subScene.getScene().setCursor(oldCursor); - oldCursor = null; - } else { - subScene.getScene().setCursor(Cursor.DEFAULT); - } - } - } - } + public void setFullScreen(boolean fullScreen) { + Window window = subScene.getScene().getWindow(); + if (window instanceof Stage) { + ((Stage) window).setFullScreenExitHint(""); + ((Stage) window).setFullScreen(fullScreen); + if (hideFullScreenMouseCursor) { + if (fullScreen) { + oldCursor = subScene.getScene().getCursor(); + subScene.getScene().setCursor(Cursor.NONE); + } else if (oldCursor != null) { + subScene.getScene().setCursor(oldCursor); + oldCursor = null; + } else { + subScene.getScene().setCursor(Cursor.DEFAULT); + } + } + } + } - public void setHideFullScreenMouseCursor(boolean hideIt) { - if (hideIt != hideFullScreenMouseCursor && isFullScreen()) { - if (hideIt) { - oldCursor = subScene.getScene().getCursor(); - subScene.getScene().setCursor(Cursor.NONE); - } else if (oldCursor != null) { - subScene.getScene().setCursor(oldCursor); - oldCursor = null; - } else { - subScene.getScene().setCursor(Cursor.DEFAULT); - } - } - hideFullScreenMouseCursor = hideIt; - } + public void setHideFullScreenMouseCursor(boolean hideIt) { + if (hideIt != hideFullScreenMouseCursor && isFullScreen()) { + if (hideIt) { + oldCursor = subScene.getScene().getCursor(); + subScene.getScene().setCursor(Cursor.NONE); + } else if (oldCursor != null) { + subScene.getScene().setCursor(oldCursor); + oldCursor = null; + } else { + subScene.getScene().setCursor(Cursor.DEFAULT); + } + } + hideFullScreenMouseCursor = hideIt; + } - /** - * @param keyOverride - * the keyOverride to set - */ - public void setKeyOverride(Predicate keyOverride) { - this.keyOverride = keyOverride; - } + /** + * @param keyOverride the keyOverride to set + */ + public void setKeyOverride(Predicate keyOverride) { + this.keyOverride = keyOverride; + } - /** - * @param keyHandler - * the keyHandler to set - */ - public void setKeyPressedHandler(Predicate keyHandler) { - this.keyPressedHandler = keyHandler; - } + /** + * @param keyHandler the keyHandler to set + */ + public void setKeyPressedHandler(Predicate keyHandler) { + this.keyPressedHandler = keyHandler; + } - /** - * @param keyReleasedHandler - * the keyReleasedHandler to set - */ - public void setKeyReleasedHandler(Predicate keyReleasedHandler) { - this.keyReleasedHandler = keyReleasedHandler; - } + /** + * @param keyReleasedHandler the keyReleasedHandler to set + */ + public void setKeyReleasedHandler(Predicate keyReleasedHandler) { + this.keyReleasedHandler = keyReleasedHandler; + } - /** - * @param keyTypedHandler - * the keyTypedHandler to set - */ - public void setKeyTypedHandler(Predicate keyTypedHandler) { - this.keyTypedHandler = keyTypedHandler; - } + /** + * @param keyTypedHandler the keyTypedHandler to set + */ + public void setKeyTypedHandler(Predicate keyTypedHandler) { + this.keyTypedHandler = keyTypedHandler; + } - public void setMouseCursor(Cursor cursor) { - subScene.getScene().setCursor(cursor); - } + public void setMouseCursor(Cursor cursor) { + subScene.getScene().setCursor(cursor); + } - public void showMouseCursor() { - subScene.getScene().setCursor(Cursor.DEFAULT); - } + public void showMouseCursor() { + subScene.getScene().setCursor(Cursor.DEFAULT); + } - public void zoomCycle() { - scaling++; - if (scaling > maxScale) - scaling = ((int) scaling) % maxScale; - recomputeLayout(true); - } + public void zoomCycle() { + scaling++; + if (scaling > maxScale) { + scaling = ((int) scaling) % maxScale; + } + recomputeLayout(true); + } - public void zoomFit() { - scaling = 0; - recomputeLayout(false); - } + public void zoomFit() { + scaling = 0; + recomputeLayout(false); + } - public void zoomIn() { - scaling = Math.min(10, currentScale + 0.2); - recomputeLayout(false); - } + public void zoomIn() { + scaling = Math.min(10, currentScale + 0.2); + recomputeLayout(false); + } - public void zoomOne() { - scaling = 1; - recomputeLayout(false); - } + public void zoomOne() { + scaling = 1; + recomputeLayout(false); + } - public void zoomOut() { - scaling = Math.max(0.1, currentScale - 0.2); - recomputeLayout(false); - } + public void zoomOut() { + scaling = Math.max(0.1, currentScale - 0.2); + recomputeLayout(false); + } } diff --git a/src/main/java/inf101/v18/gfx/gfxmode/Direction.java b/src/main/java/inf101/v18/gfx/gfxmode/Direction.java index a4758cd..eccc8e3 100644 --- a/src/main/java/inf101/v18/gfx/gfxmode/Direction.java +++ b/src/main/java/inf101/v18/gfx/gfxmode/Direction.java @@ -2,204 +2,196 @@ package inf101.v18.gfx.gfxmode; /** * @author anya - * */ public class Direction { - /** - * Construct direction from an angle - * - * @param degrees - * Angle in degrees, where 0 is (1,0) - */ - public static Direction fromDegrees(double degrees) { - return new Direction(degrees); - } - /** - * Construct direction from a vector - * - * @param x - * X direction - * @param y - * Y direction - */ - public static Direction fromVector(double x, double y) { - return new Direction(x, y); - } + /** + * Construct direction from an angle + * + * @param degrees Angle in degrees, where 0 is (1,0) + */ + public static Direction fromDegrees(double degrees) { + return new Direction(degrees); + } - private double xDir; + /** + * Construct direction from a vector + * + * @param x X direction + * @param y Y direction + */ + public static Direction fromVector(double x, double y) { + return new Direction(x, y); + } - private double yDir; + private double xDir; - /** - * Create a new direction. - * - * The direction vector will be normalised to a vector of length 1. - * - * @param degrees - * Angle of direction in degrees - */ - public Direction(double degrees) { - double radians = Math.toRadians(degrees); - this.xDir = Math.cos(radians); - this.yDir = Math.sin(radians); - normalize(); - } + private double yDir; - /** - * Create a new direction. - * - * The direction vector will be normalised to a vector of length 1. - * - * @param xDir - * X-component of direction vector - * @param yDir - * Y-component of direction vector - */ - public Direction(double xDir, double yDir) { - this.xDir = xDir; - this.yDir = yDir; - normalize(); - } + /** + * Create a new direction. + *

+ * The direction vector will be normalised to a vector of length 1. + * + * @param degrees Angle of direction in degrees + */ + public Direction(double degrees) { + double radians = Math.toRadians(degrees); + this.xDir = Math.cos(radians); + this.yDir = Math.sin(radians); + normalize(); + } - /** - * Multiply direction by distance - * - * @param distance - * @return Position delta - */ - public Point getMovement(double distance) { - return new Point(xDir * distance, -yDir * distance); - } + /** + * Create a new direction. + *

+ * The direction vector will be normalised to a vector of length 1. + * + * @param xDir X-component of direction vector + * @param yDir Y-component of direction vector + */ + public Direction(double xDir, double yDir) { + this.xDir = xDir; + this.yDir = yDir; + normalize(); + } - /** - * @return X-component of direction vector - * - * Same as the Math.cos(toRadians()) - */ - public double getX() { - return xDir; - } + /** + * Multiply direction by distance + * + * @param distance + * @return Position delta + */ + public Point getMovement(double distance) { + return new Point(xDir * distance, -yDir * distance); + } - /** - * @return Y-component of direction vector - * - * Same as the Math.sin(toRadians()) - */ - public double getY() { - return yDir; - } + /** + * @return X-component of direction vector + *

+ * Same as the Math.cos(toRadians()) + */ + public double getX() { + return xDir; + } - private void normalize() { - double l = Math.sqrt(xDir * xDir + yDir * yDir); - if (l >= 0.00001) { - xDir = xDir / l; - yDir = yDir / l; - } else if (xDir > 0) { - xDir = 1; - yDir = 0; - } else if (xDir < 0) { - xDir = -1; - yDir = 0; - } else if (yDir > 0) { - xDir = 0; - yDir = 1; - } else if (yDir < 0) { - xDir = 0; - yDir = -1; - } else { - xDir = 1; - yDir = 0; - } + /** + * @return Y-component of direction vector + *

+ * Same as the Math.sin(toRadians()) + */ + public double getY() { + return yDir; + } - } + private void normalize() { + double l = Math.sqrt(xDir * xDir + yDir * yDir); + if (l >= 0.00001) { + xDir = xDir / l; + yDir = yDir / l; + } else if (xDir > 0) { + xDir = 1; + yDir = 0; + } else if (xDir < 0) { + xDir = -1; + yDir = 0; + } else if (yDir > 0) { + xDir = 0; + yDir = 1; + } else if (yDir < 0) { + xDir = 0; + yDir = -1; + } else { + xDir = 1; + yDir = 0; + } - /** - * Translate to angle (in degrees) - * - * @return Angle in degrees, -180 .. 180 - */ - public double toDegrees() { - return Math.toDegrees(Math.atan2(yDir, xDir)); - } + } - /** - * Translate to angle (in radians) - * - * @return Angle in radians, -2π .. 2π - */ - public double toRadians() { - return Math.atan2(yDir, xDir); - } + /** + * Translate to angle (in degrees) + * + * @return Angle in degrees, -180 .. 180 + */ + public double toDegrees() { + return Math.toDegrees(Math.atan2(yDir, xDir)); + } - @Override - public String toString() { - return String.format("%.2f", toDegrees()); - } + /** + * Translate to angle (in radians) + * + * @return Angle in radians, -2π .. 2π + */ + public double toRadians() { + return Math.atan2(yDir, xDir); + } - /** - * Turn (relative) - * - * @param deltaDir - */ - public Direction turn(Direction deltaDir) { - return new Direction(xDir + deltaDir.xDir, yDir + deltaDir.yDir); - } + @Override + public String toString() { + return String.format("%.2f", toDegrees()); + } - /** - * Turn angle degrees - * - * @param angle - */ - public Direction turn(double angle) { - return turnTo(toDegrees() + angle); - } + /** + * Turn (relative) + * + * @param deltaDir + */ + public Direction turn(Direction deltaDir) { + return new Direction(xDir + deltaDir.xDir, yDir + deltaDir.yDir); + } - /** - * Turn around 180 degrees - */ - public Direction turnBack() { - return turn(180.0); - } + /** + * Turn angle degrees + * + * @param angle + */ + public Direction turn(double angle) { + return turnTo(toDegrees() + angle); + } - /** - * Turn left 90 degrees - */ - public Direction turnLeft() { - return turn(90.0); - } + /** + * Turn around 180 degrees + */ + public Direction turnBack() { + return turn(180.0); + } - /** - * Turn right 90 degrees - */ - public Direction turnRight() { - return turn(-90.0); - } + /** + * Turn left 90 degrees + */ + public Direction turnLeft() { + return turn(90.0); + } - /** - * Absolute turn - * - * @param degrees - * Angle in degrees, where 0 is (1,0) - */ - public Direction turnTo(double degrees) { - return new Direction(degrees); - } + /** + * Turn right 90 degrees + */ + public Direction turnRight() { + return turn(-90.0); + } + + /** + * Absolute turn + * + * @param degrees Angle in degrees, where 0 is (1,0) + */ + public Direction turnTo(double degrees) { + return new Direction(degrees); + } + + /** + * Turn slightly towards a directions + * + * @param dir A direction + * @param percent How much to turn (100.0 is the same as turnTo()) + */ + public Direction turnTowards(Direction dir, double percent) { + return new Direction(xDir * (1.00 - percent / 100.0) + dir.xDir * (percent / 100.0), + yDir * (1.00 - percent / 100.0) + dir.yDir * (percent / 100.0)); + // double thisAngle = toAngle(); + // double otherAngle = dir.toAngle(); + // turnTo(thisAngle*(1.00 - percent/100.0) + + // otherAngle*(percent/100.0)); + } - /** - * Turn slightly towards a directions - * - * @param dir - * A direction - * @param percent - * How much to turn (100.0 is the same as turnTo()) - */ - public Direction turnTowards(Direction dir, double percent) { - return new Direction(xDir * (1.00 - percent / 100.0) + dir.xDir * (percent / 100.0), - yDir * (1.00 - percent / 100.0) + dir.yDir * (percent / 100.0)); - // double thisAngle = toAngle(); - // double otherAngle = dir.toAngle(); - // turnTo(thisAngle*(1.00 - percent/100.0) + - // otherAngle*(percent/100.0)); - } } diff --git a/src/main/java/inf101/v18/gfx/gfxmode/Gravity.java b/src/main/java/inf101/v18/gfx/gfxmode/Gravity.java index af2044c..bf6554a 100644 --- a/src/main/java/inf101/v18/gfx/gfxmode/Gravity.java +++ b/src/main/java/inf101/v18/gfx/gfxmode/Gravity.java @@ -1,5 +1,15 @@ package inf101.v18.gfx.gfxmode; public enum Gravity { - NORTH, NORTHWEST, WEST, SOUTHWEST, SOUTH, SOUTHEAST, EAST, NORTHEAST, CENTER + + NORTH, + NORTHWEST, + WEST, + SOUTHWEST, + SOUTH, + SOUTHEAST, + EAST, + NORTHEAST, + CENTER + } diff --git a/src/main/java/inf101/v18/gfx/gfxmode/IPainter.java b/src/main/java/inf101/v18/gfx/gfxmode/IPainter.java index f0b6630..238d5dc 100644 --- a/src/main/java/inf101/v18/gfx/gfxmode/IPainter.java +++ b/src/main/java/inf101/v18/gfx/gfxmode/IPainter.java @@ -5,45 +5,45 @@ import javafx.scene.paint.Paint; public interface IPainter extends IPaintLayer { - /** - * Restore graphics settings previously stored by {@link #save()}. - * - * @return {@code this}, for sending more draw commands - */ - IPainter restore(); + /** + * Restore graphics settings previously stored by {@link #save()}. + * + * @return {@code this}, for sending more draw commands + */ + IPainter restore(); - /** - * Store graphics settings. - * - * @return {@code this}, for sending more draw commands - */ - IPainter save(); + /** + * Store graphics settings. + * + * @return {@code this}, for sending more draw commands + */ + IPainter save(); - /** - * Set colour used to drawing and filling. - * - * @param ink A colour or paint - * @return {@code this}, for sending more draw commands - */ - IPainter setInk(Paint ink); + /** + * Set colour used to drawing and filling. + * + * @param ink A colour or paint + * @return {@code this}, for sending more draw commands + */ + IPainter setInk(Paint ink); - /** - * Start drawing a shape. - * - * @return An IShape for sending shape drawing commands - */ - IShape shape(); + /** + * Start drawing a shape. + * + * @return An IShape for sending shape drawing commands + */ + IShape shape(); - /** - * Start drawing with a turtle. - * - * @return An ITurtle for sending turtle drawing commands - */ - ITurtle turtle(); - - /** - * @return Current ink, as set by {@link #setInk(Paint)} - */ - Paint getInk(); + /** + * Start drawing with a turtle. + * + * @return An ITurtle for sending turtle drawing commands + */ + ITurtle turtle(); + + /** + * @return Current ink, as set by {@link #setInk(Paint)} + */ + Paint getInk(); } \ No newline at end of file diff --git a/src/main/java/inf101/v18/gfx/gfxmode/IShape.java b/src/main/java/inf101/v18/gfx/gfxmode/IShape.java index 5b2b761..f14f2ed 100644 --- a/src/main/java/inf101/v18/gfx/gfxmode/IShape.java +++ b/src/main/java/inf101/v18/gfx/gfxmode/IShape.java @@ -6,234 +6,226 @@ import javafx.scene.shape.Shape; public interface IShape { - /** - * Add another point to the line path - * - * @param xy - * @return - */ - IShape addPoint(double x, double y); + /** + * Add another point to the line path + * + * @param x + * @param y + * @return + */ + IShape addPoint(double x, double y); - /** - * Add another point to the line path - * - * @param xy - * @return - */ - IShape addPoint(Point xy); + /** + * Add another point to the line path + * + * @param xy + * @return + */ + IShape addPoint(Point xy); - /** - * Set the arc angle for the subsequent draw commands - * - *

- * For use with {@link #arc()} - * - * @param a - * The angle, in degrees - * @return this, for adding more drawing parameters or issuing the - * draw command - */ - IShape angle(double a); + /** + * Set the arc angle for the subsequent draw commands + * + *

+ * For use with {@link #arc()} + * + * @param a The angle, in degrees + * @return this, for adding more drawing parameters or issuing the + * draw command + */ + IShape angle(double a); - /** - * Draw an arc with the current drawing parameters - * - *

- * Relevant parameters: - *

  • {@link #at(Point)}, {@link #x(double)}, {@link #gravity(double)} - *
  • {@link #length(double)} - *
  • {@link #angle(double)} - *
  • {@link #gravity(Gravity)} - *
  • {@link #stroke(Paint)}, {@link #fill(Paint)} - *
  • {@link #rotation(double)} - * - * @return this, for adding more drawing parameters or issuing the - * draw command - */ - IShape arc(); + /** + * Draw an arc with the current drawing parameters + * + *

    + * Relevant parameters: + *

  • {@link #at(Point)}, {@link #x(double)}, {@link #gravity(double)} + *
  • {@link #length(double)} + *
  • {@link #angle(double)} + *
  • {@link #gravity(Gravity)} + *
  • {@link #stroke(Paint)}, {@link #fill(Paint)} + *
  • {@link #rotation(double)} + * + * @return this, for adding more drawing parameters or issuing the + * draw command + */ + IShape arc(); - /** - * Set the (x,y)-coordinates of the next draw command - * - * @param p - * Coordinates - * @return this, for adding more drawing parameters or issuing the - * draw command - */ - IShape at(Point p); + /** + * Set the (x,y)-coordinates of the next draw command + * + * @param p Coordinates + * @return this, for adding more drawing parameters or issuing the + * draw command + */ + IShape at(Point p); - /** - * Close the line path, turning it into a polygon. - * - * @return - */ - IShape close(); + /** + * Close the line path, turning it into a polygon. + * + * @return + */ + IShape close(); - void draw(); + void draw(); - void draw(GraphicsContext context); + void draw(GraphicsContext context); - /** - * Draw an ellipse with the current drawing parameters - * - *

    - * Relevant parameters: - *

  • {@link #at(Point)}, {@link #x(double)}, {@link #gravity(double)} - *
  • {@link #width(double)}, {@link #height(double)} - *
  • {@link #gravity(Gravity)} - *
  • {@link #stroke(Paint)}, {@link #fill(Paint)} - *
  • {@link #rotation(double)} - * - * @return this, for adding more drawing parameters or issuing the - * draw command - */ - IShape ellipse(); + /** + * Draw an ellipse with the current drawing parameters + * + *

    + * Relevant parameters: + *

  • {@link #at(Point)}, {@link #x(double)}, {@link #gravity(double)} + *
  • {@link #width(double)}, {@link #height(double)} + *
  • {@link #gravity(Gravity)} + *
  • {@link #stroke(Paint)}, {@link #fill(Paint)} + *
  • {@link #rotation(double)} + * + * @return this, for adding more drawing parameters or issuing the + * draw command + */ + IShape ellipse(); - /** - * Fill the current shape - * - * @return this, for adding more drawing parameters or issuing the - * draw command - */ - IShape fill(); + /** + * Fill the current shape + * + * @return this, for adding more drawing parameters or issuing the + * draw command + */ + IShape fill(); - /** - * Set fill colour for the subsequent draw commands - * - * @return this, for adding more drawing parameters or issuing the - * draw command - */ - IShape fillPaint(Paint p); + /** + * Set fill colour for the subsequent draw commands + * + * @return this, for adding more drawing parameters or issuing the + * draw command + */ + IShape fillPaint(Paint p); - /** - * Set gravity for the subsequent draw commands - * - * Gravity determines the point on the shape that will be used for positioning - * and rotation. - * - * @param g - * The gravity - * @return - */ - IShape gravity(Gravity g); + /** + * Set gravity for the subsequent draw commands + *

    + * Gravity determines the point on the shape that will be used for positioning + * and rotation. + * + * @param g The gravity + * @return + */ + IShape gravity(Gravity g); - /** - * Set the height of the next draw command - * - * @param h - * The height - * @return this, for adding more drawing parameters or issuing the - * draw command - */ - IShape height(double h); + /** + * Set the height of the next draw command + * + * @param h The height + * @return this, for adding more drawing parameters or issuing the + * draw command + */ + IShape height(double h); - /** - * Set the length of the following draw commands - * - *

    - * For use with {@link #line()} and {@link #arc()} - * - * @param l - * The length - * @return this, for adding more drawing parameters or issuing the - * draw command - */ - IShape length(double l); + /** + * Set the length of the following draw commands + * + *

    + * For use with {@link #line()} and {@link #arc()} + * + * @param l The length + * @return this, for adding more drawing parameters or issuing the + * draw command + */ + IShape length(double l); - /** - * Draw a line with the current drawing parameters - * - *

    - * Relevant parameters: - *

  • {@link #at(Point)}, {@link #x(double)}, {@link #gravity(double)} - *
  • {@link #length(double)} - *
  • {@link #angle(double)} - *
  • {@link #gravity(Gravity)} (flattened to the horizontal axis, so, e.g., - * {@link Gravity#NORTH} = {@link Gravity#SOUTH} = {@link Gravity#CENTER}) - *
  • {@link #stroke(Paint)} - *
  • {@link #rotation(double)} - * - * @return this, for adding more drawing parameters or issuing the - * draw command - */ - IShape line(); + /** + * Draw a line with the current drawing parameters + * + *

    + * Relevant parameters: + *

  • {@link #at(Point)}, {@link #x(double)}, {@link #gravity(double)} + *
  • {@link #length(double)} + *
  • {@link #angle(double)} + *
  • {@link #gravity(Gravity)} (flattened to the horizontal axis, so, e.g., + * {@link Gravity#NORTH} = {@link Gravity#SOUTH} = {@link Gravity#CENTER}) + *
  • {@link #stroke(Paint)} + *
  • {@link #rotation(double)} + * + * @return this, for adding more drawing parameters or issuing the + * draw command + */ + IShape line(); - /** - * Draw a rectangle with the current drawing parameters - * - *

    - * Relevant parameters: - *

  • {@link #at(Point)}, {@link #x(double)}, {@link #gravity(double)} - *
  • {@link #width(double)}, {@link #height(double)} - *
  • {@link #gravity(Gravity)} - *
  • {@link #stroke(Paint)}, {@link #fill(Paint)} - *
  • {@link #rotation(double)} - * - * @return this, for adding more drawing parameters or issuing the - * draw command - */ - IShape rectangle(); + /** + * Draw a rectangle with the current drawing parameters + * + *

    + * Relevant parameters: + *

  • {@link #at(Point)}, {@link #x(double)}, {@link #gravity(double)} + *
  • {@link #width(double)}, {@link #height(double)} + *
  • {@link #gravity(Gravity)} + *
  • {@link #stroke(Paint)}, {@link #fill(Paint)} + *
  • {@link #rotation(double)} + * + * @return this, for adding more drawing parameters or issuing the + * draw command + */ + IShape rectangle(); - /** - * Sets rotation for subsequent draw commands. - * - *

    - * Shapes will be rotate around the {@link #gravity(Gravity)} point. - * - * @param angle - * Rotation in degrees - * @return - */ - IShape rotation(double angle); + /** + * Sets rotation for subsequent draw commands. + * + *

    + * Shapes will be rotate around the {@link #gravity(Gravity)} point. + * + * @param angle Rotation in degrees + * @return + */ + IShape rotation(double angle); - /** - * Stroke the current shape - * - * @return this, for adding more drawing parameters or issuing the - * draw command - */ - IShape stroke(); + /** + * Stroke the current shape + * + * @return this, for adding more drawing parameters or issuing the + * draw command + */ + IShape stroke(); - /** - * Set stroke colour for the subsequent draw commands - * - * @return this, for adding more drawing parameters or issuing the - * draw command - */ - IShape strokePaint(Paint p); + /** + * Set stroke colour for the subsequent draw commands + * + * @return this, for adding more drawing parameters or issuing the + * draw command + */ + IShape strokePaint(Paint p); - Shape toFXShape(); + Shape toFXShape(); - String toSvg(); + String toSvg(); - /** - * Set the width of the next draw command - * - * @param w - * The width - * @return this, for adding more drawing parameters or issuing the - * draw command - */ - IShape width(double w); + /** + * Set the width of the next draw command + * + * @param w The width + * @return this, for adding more drawing parameters or issuing the + * draw command + */ + IShape width(double w); - /** - * Set the x-coordinate of the next draw command - * - * @param x - * Coordinate - * @return this, for adding more drawing parameters or issuing the - * draw command - */ - IShape x(double x); + /** + * Set the x-coordinate of the next draw command + * + * @param x Coordinate + * @return this, for adding more drawing parameters or issuing the + * draw command + */ + IShape x(double x); - /** - * Set the y-coordinate of the next draw command - * - * @param y - * Coordinate - * @return this, for adding more drawing parameters or issuing the - * draw command - */ - IShape y(double y); + /** + * Set the y-coordinate of the next draw command + * + * @param y Coordinate + * @return this, for adding more drawing parameters or issuing the + * draw command + */ + IShape y(double y); } diff --git a/src/main/java/inf101/v18/gfx/gfxmode/ITurtle.java b/src/main/java/inf101/v18/gfx/gfxmode/ITurtle.java index 4897441..95089d8 100644 --- a/src/main/java/inf101/v18/gfx/gfxmode/ITurtle.java +++ b/src/main/java/inf101/v18/gfx/gfxmode/ITurtle.java @@ -2,269 +2,253 @@ package inf101.v18.gfx.gfxmode; public interface ITurtle extends IPainter { - /** - * This method is used to convert the turtle to an other type, determined by the - * class object given as an argument. - *

    - * This can be used to access extra functionality not provided by this - * interface, such as direct access to the underlying graphics context. - * - * @param clazz - * @return This object or an appropriate closely related object of the given - * time; or null if no appropriate object can be found - */ - T as(Class clazz); + /** + * This method is used to convert the turtle to another type, determined by the + * class object given as an argument. + *

    + * This can be used to access extra functionality not provided by this + * interface, such as direct access to the underlying graphics context. + * + * @param clazz + * @return This object or an appropriate closely related object of the given + * time; or null if no appropriate object can be found + */ + T as(Class clazz); - /** - * Move to the given position while drawing a curve - * - *

    - * The resulting curve is a cubic Bézier curve with the control points located - * at getPos().move(getDirection, startControl) and - * to.move(Direction.fromDegrees(endAngle+180), endControl). - *

    - * The turtle is left at point to, facing endAngle. - *

    - * The turtle will start out moving in its current direction, aiming for a point - * startControl pixels away, then smoothly turning towards its - * goal. It will approach the to point moving in the direction - * endAngle (an absolute bearing, with 0° pointing right and 90° - * pointing up). - * - * @param to - * Position to move to - * @param startControl - * Distance to the starting control point. - * @return {@code this}, for sending more draw commands - */ - ITurtle curveTo(Point to, double startControl, double endAngle, double endControl); + /** + * Move to the given position while drawing a curve + * + *

    + * The resulting curve is a cubic Bézier curve with the control points located + * at getPos().move(getDirection, startControl) and + * to.move(Direction.fromDegrees(endAngle+180), endControl). + *

    + * The turtle is left at point to, facing endAngle. + *

    + * The turtle will start out moving in its current direction, aiming for a point + * startControl pixels away, then smoothly turning towards its + * goal. It will approach the to point moving in the direction + * endAngle (an absolute bearing, with 0° pointing right and 90° + * pointing up). + * + * @param to Position to move to + * @param startControl Distance to the starting control point. + * @return {@code this}, for sending more draw commands + */ + ITurtle curveTo(Point to, double startControl, double endAngle, double endControl); - void debugTurtle(); + void debugTurtle(); - /** - * Move forward the given distance while drawing a line - * - * @param dist - * Distance to move - * @return {@code this}, for sending more draw commands - */ - ITurtle draw(double dist); + /** + * Move forward the given distance while drawing a line + * + * @param dist Distance to move + * @return {@code this}, for sending more draw commands + */ + ITurtle draw(double dist); - /** - * Move to the given position while drawing a line - * - * @param x - * X-position to move to - * @param y - * Y-position to move to - * @return {@code this}, for sending more draw commands - */ - ITurtle drawTo(double x, double y); + /** + * Move to the given position while drawing a line + * + * @param x X-position to move to + * @param y Y-position to move to + * @return {@code this}, for sending more draw commands + */ + ITurtle drawTo(double x, double y); - /** - * Move to the given position while drawing a line - * - * @param to - * Position to move to - * @return {@code this}, for sending more draw commands - */ - ITurtle drawTo(Point to); + /** + * Move to the given position while drawing a line + * + * @param to Position to move to + * @return {@code this}, for sending more draw commands + */ + ITurtle drawTo(Point to); - /** - * @return The current angle of the turtle, with 0° pointing to the right and - * 90° pointing up. Same as {@link #getDirection()}.getAngle() - */ - double getAngle(); + /** + * @return The current angle of the turtle, with 0° pointing to the right and + * 90° pointing up. Same as {@link #getDirection()}.getAngle() + */ + double getAngle(); - /** - * @return The current direction of the turtle. Same as calling - * new Direction(getAngle()) - */ - Direction getDirection(); + /** + * @return The current direction of the turtle. Same as calling + * new Direction(getAngle()) + */ + Direction getDirection(); - /** - * @return The current position of the turtle. - */ - Point getPos(); + /** + * @return The current position of the turtle. + */ + Point getPos(); - /** - * Move a distance without drawing. - * - * @param dist - * Distance to move - * @return {@code this}, for sending more draw commands - */ - ITurtle jump(double dist); + /** + * Move a distance without drawing. + * + * @param dist Distance to move + * @return {@code this}, for sending more draw commands + */ + ITurtle jump(double dist); - /** - * Move a position without drawing. - * - * @param x - * X position to move to - * @param y - * Y position to move to - * @return {@code this}, for sending more draw commands - */ - ITurtle jumpTo(double x, double y); + /** + * Move a position without drawing. + * + * @param x X position to move to + * @param y Y position to move to + * @return {@code this}, for sending more draw commands + */ + ITurtle jumpTo(double x, double y); - /** - * Move a position without drawing. - * - * @param to - * X,Y position to move to - * @return {@code this}, for sending more draw commands - */ - ITurtle jumpTo(Point to); + /** + * Move a position without drawing. + * + * @param to X,Y position to move to + * @return {@code this}, for sending more draw commands + */ + ITurtle jumpTo(Point to); - /** - * Draw a line from the current position to the given position. - * - *

    - * This method does not change the turtle position. - * - * @param to - * Other end-point of the line - * @return {@code this}, for sending more draw commands - */ - ITurtle line(Point to); + /** + * Draw a line from the current position to the given position. + * + *

    + * This method does not change the turtle position. + * + * @param to Other end-point of the line + * @return {@code this}, for sending more draw commands + */ + ITurtle line(Point to); - /** - * Set the size of the turtle's pen - * - * @param pixels - * Line width, in pixels - * @return {@code this}, for sending more draw commands - * @requires pixels >= 0 - */ - ITurtle setPenSize(double pixels); + /** + * Set the size of the turtle's pen + * + * @param pixels Line width, in pixels + * @return {@code this}, for sending more draw commands + * @requires pixels >= 0 + */ + ITurtle setPenSize(double pixels); - /** - * Start drawing a shape at the current turtle position. - * - *

    - * The shape's default origin and rotation will be set to the turtle's current - * position and direction, but can be modified with {@link IShape#at(Point)} and - * {@link IShape#rotation(double)}. - *

    - * The turtle's position and attributes are unaffected by drawing the shape. - * - * @return An IDrawParams object for setting up and drawing the shape - */ - @Override - IShape shape(); + /** + * Start drawing a shape at the current turtle position. + * + *

    + * The shape's default origin and rotation will be set to the turtle's current + * position and direction, but can be modified with {@link IShape#at(Point)} and + * {@link IShape#rotation(double)}. + *

    + * The turtle's position and attributes are unaffected by drawing the shape. + * + * @return An IDrawParams object for setting up and drawing the shape + */ + @Override + IShape shape(); - /** - * Change direction the given number of degrees (relative to the current - * direction). - * - *

    - * Positive degrees turn left while negative degrees turn - * right. - * - * @param degrees - * @return {@code this}, for sending more draw commands - */ - ITurtle turn(double degrees); + /** + * Change direction the given number of degrees (relative to the current + * direction). + * + *

    + * Positive degrees turn left while negative degrees turn + * right. + * + * @param degrees + * @return {@code this}, for sending more draw commands + */ + ITurtle turn(double degrees); - /** - * Turn 180°. - * - *

    - * Same as turn(180) and turn(-180). - * - * @return {@code this}, for sending more draw commands - */ - ITurtle turnAround(); + /** + * Turn 180°. + * + *

    + * Same as turn(180) and turn(-180). + * + * @return {@code this}, for sending more draw commands + */ + ITurtle turnAround(); - /** - * Turn left 90°. - * - *

    - * Same as turn(90). - * - * @return {@code this}, for sending more draw commands - */ - ITurtle turnLeft(); + /** + * Turn left 90°. + * + *

    + * Same as turn(90). + * + * @return {@code this}, for sending more draw commands + */ + ITurtle turnLeft(); - /** - * Turn left. - * - *

    - * Same as turn(degrees). - * - * @return {@code this}, for sending more draw commands - * @requires degrees >= 0 - */ - ITurtle turnLeft(double degrees); + /** + * Turn left. + * + *

    + * Same as turn(degrees). + * + * @return {@code this}, for sending more draw commands + * @requires degrees >= 0 + */ + ITurtle turnLeft(double degrees); - /** - * Turn right 90°. - * - *

    - * Same as turn(-90). - * - * @return {@code this}, for sending more draw commands - */ - ITurtle turnRight(); + /** + * Turn right 90°. + * + *

    + * Same as turn(-90). + * + * @return {@code this}, for sending more draw commands + */ + ITurtle turnRight(); - /** - * Turn left. - * - *

    - * Same as turn(-degrees). - * - * @return {@code this}, for sending more draw commands - * @requires degrees >= 0 - */ - ITurtle turnRight(double degrees); + /** + * Turn left. + * + *

    + * Same as turn(-degrees). + * + * @return {@code this}, for sending more draw commands + * @requires degrees >= 0 + */ + ITurtle turnRight(double degrees); - /** - * Turn to the given bearing. - * - *

    - * 0° is due right, 90° is up. - * - * @param degrees - * Bearing, in degrees - * @return {@code this}, for sending more draw commands - */ - ITurtle turnTo(double degrees); + /** + * Turn to the given bearing. + * + *

    + * 0° is due right, 90° is up. + * + * @param degrees Bearing, in degrees + * @return {@code this}, for sending more draw commands + */ + ITurtle turnTo(double degrees); - /** - * Turn towards the given bearing. - * - *

    - * Use this method to turn slightly towards something. - * - *

    - * 0° is due right, 90° is up. - * - * @param degrees - * Bearing, in degrees - * @param percent - * How far to turn, in degrees. - * @return {@code this}, for sending more draw commands - */ - ITurtle turnTowards(double degrees, double percent); + /** + * Turn towards the given bearing. + * + *

    + * Use this method to turn slightly towards something. + * + *

    + * 0° is due right, 90° is up. + * + * @param degrees Bearing, in degrees + * @param percent How far to turn, in degrees. + * @return {@code this}, for sending more draw commands + */ + ITurtle turnTowards(double degrees, double percent); - /** - * Jump (without drawing) to the given relative position. - *

    - * The new position will be equal to getPos().move(relPos). - * - * @param relPos - * A position, interpreted relative to current position - * @return {@code this}, for sending more draw commands - */ - ITurtle jump(Point relPos); + /** + * Jump (without drawing) to the given relative position. + *

    + * The new position will be equal to getPos().move(relPos). + * + * @param relPos A position, interpreted relative to current position + * @return {@code this}, for sending more draw commands + */ + ITurtle jump(Point relPos); - /** - * Move to the given relative position while drawing a line - *

    - * The new position will be equal to getPos().move(relPos). - * - * @return {@code this}, for sending more draw commands - */ - ITurtle draw(Point relPos); + /** + * Move to the given relative position while drawing a line + *

    + * The new position will be equal to getPos().move(relPos). + * + * @return {@code this}, for sending more draw commands + */ + ITurtle draw(Point relPos); } \ No newline at end of file diff --git a/src/main/java/inf101/v18/gfx/gfxmode/Point.java b/src/main/java/inf101/v18/gfx/gfxmode/Point.java index 997a2a5..66e893b 100644 --- a/src/main/java/inf101/v18/gfx/gfxmode/Point.java +++ b/src/main/java/inf101/v18/gfx/gfxmode/Point.java @@ -1,116 +1,113 @@ package inf101.v18.gfx.gfxmode; public class Point { - private final double x; - private final double y; + private final double x; + private final double y; - public Point(double x, double y) { - this.x = x; - this.y = y; - } + public Point(double x, double y) { + this.x = x; + this.y = y; + } - /** - * Calculate direction towards other position - * - * @param otherPos - * @return - */ - public Direction directionTo(Point otherPos) { - return new Direction(otherPos.x - x, otherPos.y - y); - } + /** + * Calculate direction towards other position + * + * @param otherPos + * @return + */ + public Direction directionTo(Point otherPos) { + return new Direction(otherPos.x - x, otherPos.y - y); + } - /** - * Calculate distance to other position - * - * @param otherPos - * @return - */ - public double distanceTo(Point otherPos) { - return Math.sqrt(Math.pow(x - otherPos.x, 2) + Math.pow(y - otherPos.y, 2)); - } + /** + * Calculate distance to other position + * + * @param otherPos + * @return + */ + public double distanceTo(Point otherPos) { + return Math.sqrt(Math.pow(x - otherPos.x, 2) + Math.pow(y - otherPos.y, 2)); + } - /** - * @return The X coordinate - */ - public double getX() { - return x; - } + /** + * @return The X coordinate + */ + public double getX() { + return x; + } - /** - * @return The Y coordinate - */ - public double getY() { - return y; - } + /** + * @return The Y coordinate + */ + public double getY() { + return y; + } - /** - * Relative move - * - * @param dir - * Direction - * @param distance - * Distance to move - */ - public Point move(Direction dir, double distance) { - return new Point(x + dir.getX() * distance, y - dir.getY() * distance); - } + /** + * Relative move + * + * @param dir Direction + * @param distance Distance to move + */ + public Point move(Direction dir, double distance) { + return new Point(x + dir.getX() * distance, y - dir.getY() * distance); + } - /** - * Relative move - * - * @param deltaX - * @param deltaY - * @return A new point at x+deltaX, y+deltaY - */ - public Point move(double deltaX, double deltaY) { - return new Point(x + deltaX, y + deltaY); - } + /** + * Relative move + * + * @param deltaX + * @param deltaY + * @return A new point at x+deltaX, y+deltaY + */ + public Point move(double deltaX, double deltaY) { + return new Point(x + deltaX, y + deltaY); + } - /** - * Relative move - * - * @param deltaPos - */ - public Point move(Point deltaPos) { - return new Point(x + deltaPos.x, y + deltaPos.y); - } + /** + * Relative move + * + * @param deltaPos + */ + public Point move(Point deltaPos) { + return new Point(x + deltaPos.x, y + deltaPos.y); + } - /** - * Change position - * - * @param newX - * the new X coordinate - * @param newY - * the new Y coordinate - * @return A new point at newX, newY - */ - public Point moveTo(double newX, double newY) { - return new Point(newX, newY); - } + /** + * Change position + * + * @param newX the new X coordinate + * @param newY the new Y coordinate + * @return A new point at newX, newY + */ + public Point moveTo(double newX, double newY) { + return new Point(newX, newY); + } - @Override - public String toString() { - return String.format("(%.2f,%.2f)", x, y); - } + @Override + public String toString() { + return String.format("(%.2f,%.2f)", x, y); + } - /** - * Multiply this point by a scale factor. - * - * @param factor A scale factor - * @return A new Point, (getX()*factor, getY()*factor) - */ - public Point scale(double factor) { - return new Point(x*factor, y*factor); - } + /** + * Multiply this point by a scale factor. + * + * @param factor A scale factor + * @return A new Point, (getX()*factor, getY()*factor) + */ + public Point scale(double factor) { + return new Point(x * factor, y * factor); + } - /** - * Find difference between points. - *

    - * The returned value will be such that this.move(deltaTo(point)).equals(point). - * @param point Another point - * @return A new Point, (point.getX()-getX(), point.getY()-getY()) - */ - public Point deltaTo(Point point) { - return new Point(point.x-x, point.y-y); - } + /** + * Find difference between points. + *

    + * The returned value will be such that this.move(deltaTo(point)).equals(point). + * + * @param point Another point + * @return A new Point, (point.getX()-getX(), point.getY()-getY()) + */ + public Point deltaTo(Point point) { + return new Point(point.x - x, point.y - y); + } } diff --git a/src/main/java/inf101/v18/gfx/gfxmode/ShapePainter.java b/src/main/java/inf101/v18/gfx/gfxmode/ShapePainter.java index d7f4e17..c385fe7 100644 --- a/src/main/java/inf101/v18/gfx/gfxmode/ShapePainter.java +++ b/src/main/java/inf101/v18/gfx/gfxmode/ShapePainter.java @@ -1,329 +1,330 @@ package inf101.v18.gfx.gfxmode; -import java.util.List; - import javafx.scene.canvas.GraphicsContext; import javafx.scene.paint.Paint; import javafx.scene.shape.Shape; +import java.util.List; + public class ShapePainter implements IShape { - private abstract static class DrawCommand { - protected double calcX(Gravity g, double w) { - switch (g) { - default: - case CENTER: - return w / 2; - case EAST: - return w; - case NORTH: - return w / 2; - case NORTHEAST: - return w; - case NORTHWEST: - return 0; - case SOUTH: - return w / 2; - case SOUTHEAST: - return w; - case SOUTHWEST: - return 0; - case WEST: - return 0; - } - } + private abstract static class DrawCommand { + protected double calcX(Gravity g, double w) { + switch (g) { + default: + case CENTER: + case SOUTH: + case NORTH: + return w / 2; + case EAST: + case NORTHEAST: + case SOUTHEAST: + return w; + case NORTHWEST: + case WEST: + case SOUTHWEST: + return 0; + } + } - protected double calcY(Gravity g, double h) { - switch (g) { - default: - case CENTER: - return h / 2; - case EAST: - return h / 2; - case NORTH: - return 0; - case NORTHEAST: - return 0; - case NORTHWEST: - return 0; - case SOUTH: - return h; - case SOUTHEAST: - return h; - case SOUTHWEST: - return h; - case WEST: - return h / 2; - } - } + protected double calcY(Gravity g, double h) { + switch (g) { + default: + case CENTER: + case EAST: + case WEST: + return h / 2; + case NORTH: + case NORTHWEST: + case NORTHEAST: + return 0; + case SOUTH: + case SOUTHWEST: + case SOUTHEAST: + return h; + } + } - public void fill(GraphicsContext ctx, ShapePainter p) { - ctx.save(); - ctx.setFill(p.fill); - ctx.translate(p.x, p.y); - if (p.rot != 0) - ctx.rotate(-p.rot); - fillIt(ctx, p); - ctx.restore(); - } + public void fill(GraphicsContext ctx, ShapePainter p) { + ctx.save(); + ctx.setFill(p.fill); + ctx.translate(p.x, p.y); + if (p.rot != 0) { + ctx.rotate(-p.rot); + } + fillIt(ctx, p); + ctx.restore(); + } - protected abstract void fillIt(GraphicsContext ctx, ShapePainter p); + protected abstract void fillIt(GraphicsContext ctx, ShapePainter p); - // public abstract Shape toFXShape(DrawParams p); - // - // public abstract String toSvg(DrawParams p); + // public abstract Shape toFXShape(DrawParams p); + // + // public abstract String toSvg(DrawParams p); - public void stroke(GraphicsContext ctx, ShapePainter p) { - ctx.save(); - ctx.setStroke(p.stroke); - if (p.strokeWidth != 0) - ctx.setLineWidth(p.strokeWidth); - ctx.translate(p.x, p.y); - if (p.rot != 0) - ctx.rotate(-p.rot); - strokeIt(ctx, p); - ctx.restore(); - } + public void stroke(GraphicsContext ctx, ShapePainter p) { + ctx.save(); + ctx.setStroke(p.stroke); + if (p.strokeWidth != 0) { + ctx.setLineWidth(p.strokeWidth); + } + ctx.translate(p.x, p.y); + if (p.rot != 0) { + ctx.rotate(-p.rot); + } + strokeIt(ctx, p); + ctx.restore(); + } - protected abstract void strokeIt(GraphicsContext ctx, ShapePainter p); - } + protected abstract void strokeIt(GraphicsContext ctx, ShapePainter p); + } - private static class DrawEllipse extends DrawCommand { + private static class DrawEllipse extends DrawCommand { - @Override - public void fillIt(GraphicsContext ctx, ShapePainter p) { - ctx.fillOval(-calcX(p.gravity, p.w), -calcY(p.gravity, p.h), p.w, p.h); - } + @Override + public void fillIt(GraphicsContext ctx, ShapePainter p) { + ctx.fillOval(-calcX(p.gravity, p.w), -calcY(p.gravity, p.h), p.w, p.h); + } - @Override - public void strokeIt(GraphicsContext ctx, ShapePainter p) { - ctx.strokeOval(-calcX(p.gravity, p.w), -calcY(p.gravity, p.h), p.w, p.h); - } - } + @Override + public void strokeIt(GraphicsContext ctx, ShapePainter p) { + ctx.strokeOval(-calcX(p.gravity, p.w), -calcY(p.gravity, p.h), p.w, p.h); + } + } - private static class DrawLine extends DrawCommand { + private static class DrawLine extends DrawCommand { - @Override - public void fillIt(GraphicsContext ctx, ShapePainter p) { - if (p.lineSegments != null) { - int nPoints = (p.lineSegments.size() / 2) + 1; - double xs[] = new double[nPoints]; - double ys[] = new double[nPoints]; - xs[0] = -calcX(p.gravity, p.w); - ys[0] = -calcY(p.gravity, p.h); - for (int i = 0; i < p.lineSegments.size(); i++) { - xs[i] = p.lineSegments.get(i * 2) - p.x; - ys[i] = p.lineSegments.get(i * 2 + 1) - p.y; - } - ctx.fillPolygon(xs, ys, nPoints); - } - } + @Override + public void fillIt(GraphicsContext ctx, ShapePainter p) { + if (p.lineSegments != null) { + int nPoints = (p.lineSegments.size() / 2) + 1; + double[] xs = new double[nPoints]; + double[] ys = new double[nPoints]; + xs[0] = -calcX(p.gravity, p.w); + ys[0] = -calcY(p.gravity, p.h); + for (int i = 0; i < p.lineSegments.size(); i++) { + xs[i] = p.lineSegments.get(i * 2) - p.x; + ys[i] = p.lineSegments.get(i * 2 + 1) - p.y; + } + ctx.fillPolygon(xs, ys, nPoints); + } + } - @Override - public void strokeIt(GraphicsContext ctx, ShapePainter p) { - if (p.lineSegments == null) { - double x = -calcX(p.gravity, p.w); - double y = -calcY(p.gravity, p.h); - ctx.strokeLine(x, y, x + p.w, y + p.h); - } else { - int nPoints = (p.lineSegments.size() / 2) + 1; - double xs[] = new double[nPoints]; - double ys[] = new double[nPoints]; - xs[0] = -calcX(p.gravity, p.w); - ys[0] = -calcY(p.gravity, p.h); - for (int i = 0; i < p.lineSegments.size(); i++) { - xs[i] = p.lineSegments.get(i * 2) - p.x; - ys[i] = p.lineSegments.get(i * 2 + 1) - p.y; - } - if (p.closed) - ctx.strokePolygon(xs, ys, nPoints); - else - ctx.strokePolyline(xs, ys, nPoints); - } - } - } + @Override + public void strokeIt(GraphicsContext ctx, ShapePainter p) { + if (p.lineSegments == null) { + double x = -calcX(p.gravity, p.w); + double y = -calcY(p.gravity, p.h); + ctx.strokeLine(x, y, x + p.w, y + p.h); + } else { + int nPoints = (p.lineSegments.size() / 2) + 1; + double[] xs = new double[nPoints]; + double[] ys = new double[nPoints]; + xs[0] = -calcX(p.gravity, p.w); + ys[0] = -calcY(p.gravity, p.h); + for (int i = 0; i < p.lineSegments.size(); i++) { + xs[i] = p.lineSegments.get(i * 2) - p.x; + ys[i] = p.lineSegments.get(i * 2 + 1) - p.y; + } + if (p.closed) { + ctx.strokePolygon(xs, ys, nPoints); + } else { + ctx.strokePolyline(xs, ys, nPoints); + } + } + } + } - private static class DrawRectangle extends DrawCommand { + private static class DrawRectangle extends DrawCommand { - @Override - public void fillIt(GraphicsContext ctx, ShapePainter p) { - ctx.fillRect(-calcX(p.gravity, p.w), -calcY(p.gravity, p.h), p.w, p.h); - } + @Override + public void fillIt(GraphicsContext ctx, ShapePainter p) { + ctx.fillRect(-calcX(p.gravity, p.w), -calcY(p.gravity, p.h), p.w, p.h); + } - @Override - public void strokeIt(GraphicsContext ctx, ShapePainter p) { - ctx.strokeRect(-calcX(p.gravity, p.w), -calcY(p.gravity, p.h), p.w, p.h); - } - } + @Override + public void strokeIt(GraphicsContext ctx, ShapePainter p) { + ctx.strokeRect(-calcX(p.gravity, p.w), -calcY(p.gravity, p.h), p.w, p.h); + } + } - private double x = 0, y = 0, w = 0, h = 0, rot = 0, strokeWidth = 0; - private List lineSegments = null; - private Paint fill = null; - private Paint stroke = null; + private double x = 0; + private double y = 0; + private double w = 0; + private double h = 0; + private double rot = 0; + private final double strokeWidth = 0; + private final List lineSegments = null; + private Paint fill = null; + private Paint stroke = null; - private Gravity gravity = Gravity.CENTER; + private Gravity gravity = Gravity.CENTER; - private DrawCommand cmd = null; + private DrawCommand cmd = null; - private boolean closed = false; + private boolean closed = false; - private final GraphicsContext context; + private final GraphicsContext context; - public ShapePainter(GraphicsContext context) { - super(); - this.context = context; - } + public ShapePainter(GraphicsContext context) { + super(); + this.context = context; + } - @Override - public IShape addPoint(double x, double y) { - lineSegments.add(x); - lineSegments.add(y); - return this; - } + @Override + public IShape addPoint(double x, double y) { + lineSegments.add(x); + lineSegments.add(y); + return this; + } - @Override - public IShape addPoint(Point xy) { - lineSegments.add(xy.getX()); - lineSegments.add(xy.getY()); - return this; - } + @Override + public IShape addPoint(Point xy) { + lineSegments.add(xy.getX()); + lineSegments.add(xy.getY()); + return this; + } - @Override - public ShapePainter angle(double a) { - return this; - } + @Override + public ShapePainter angle(double a) { + return this; + } - @Override - public IShape arc() { - throw new UnsupportedOperationException(); - } + @Override + public IShape arc() { + throw new UnsupportedOperationException(); + } - @Override - public ShapePainter at(Point p) { - if (p != null) { - this.x = p.getX(); - this.y = p.getY(); - } else { - this.x = 0; - this.y = 0; - } - return this; - } + @Override + public ShapePainter at(Point p) { + if (p != null) { + this.x = p.getX(); + this.y = p.getY(); + } else { + this.x = 0; + this.y = 0; + } + return this; + } - @Override - public IShape close() { - closed = true; - return this; - } + @Override + public IShape close() { + closed = true; + return this; + } - @Override - public void draw() { - draw(context); - } + @Override + public void draw() { + draw(context); + } - @Override - public void draw(GraphicsContext context) { - if (cmd != null && context != null) { - if (fill != null) - cmd.fill(context, this); - if (stroke != null) - cmd.stroke(context, this); - } - } + @Override + public void draw(GraphicsContext context) { + if (cmd != null && context != null) { + if (fill != null) { + cmd.fill(context, this); + } + if (stroke != null) { + cmd.stroke(context, this); + } + } + } - @Override - public IShape ellipse() { - cmd = new DrawEllipse(); - return this; - } + @Override + public IShape ellipse() { + cmd = new DrawEllipse(); + return this; + } - @Override - public ShapePainter fill() { - if (cmd != null && context != null) - cmd.fill(context, this); - return this; - } + @Override + public ShapePainter fill() { + if (cmd != null && context != null) { + cmd.fill(context, this); + } + return this; + } - @Override - public ShapePainter fillPaint(Paint p) { - fill = p; - return this; - } + @Override + public ShapePainter fillPaint(Paint p) { + fill = p; + return this; + } - @Override - public ShapePainter gravity(Gravity g) { - gravity = g; - return this; - } + @Override + public ShapePainter gravity(Gravity g) { + gravity = g; + return this; + } - @Override - public ShapePainter height(double h) { - this.h = h; - return this; - } + @Override + public ShapePainter height(double h) { + this.h = h; + return this; + } - @Override - public ShapePainter length(double l) { - w = l; - h = l; - return this; - } + @Override + public ShapePainter length(double l) { + w = l; + h = l; + return this; + } - @Override - public IShape line() { - cmd = new DrawLine(); - return this; - } + @Override + public IShape line() { + cmd = new DrawLine(); + return this; + } - @Override - public IShape rectangle() { - cmd = new DrawRectangle(); - return this; - } + @Override + public IShape rectangle() { + cmd = new DrawRectangle(); + return this; + } - @Override - public ShapePainter rotation(double angle) { - rot = angle; - return this; - } + @Override + public ShapePainter rotation(double angle) { + rot = angle; + return this; + } - @Override - public ShapePainter stroke() { - if (cmd != null && context != null) - cmd.stroke(context, this); - return this; - } + @Override + public ShapePainter stroke() { + if (cmd != null && context != null) { + cmd.stroke(context, this); + } + return this; + } - @Override - public ShapePainter strokePaint(Paint p) { - stroke = p; - return this; - } + @Override + public ShapePainter strokePaint(Paint p) { + stroke = p; + return this; + } - @Override - public Shape toFXShape() { - throw new UnsupportedOperationException(); - } + @Override + public Shape toFXShape() { + throw new UnsupportedOperationException(); + } - @Override - public String toSvg() { - throw new UnsupportedOperationException(); - } + @Override + public String toSvg() { + throw new UnsupportedOperationException(); + } - @Override - public ShapePainter width(double w) { - this.w = w; - return this; - } + @Override + public ShapePainter width(double w) { + this.w = w; + return this; + } - @Override - public ShapePainter x(double x) { - this.x = x; - return this; - } + @Override + public ShapePainter x(double x) { + this.x = x; + return this; + } - @Override - public ShapePainter y(double y) { - this.y = y; - return this; - } + @Override + public ShapePainter y(double y) { + this.y = y; + return this; + } } diff --git a/src/main/java/inf101/v18/gfx/gfxmode/TurtlePainter.java b/src/main/java/inf101/v18/gfx/gfxmode/TurtlePainter.java index eb8e86d..8c8b0fb 100644 --- a/src/main/java/inf101/v18/gfx/gfxmode/TurtlePainter.java +++ b/src/main/java/inf101/v18/gfx/gfxmode/TurtlePainter.java @@ -1,8 +1,5 @@ package inf101.v18.gfx.gfxmode; -import java.util.ArrayList; -import java.util.List; - import inf101.v18.gfx.IPaintLayer; import inf101.v18.gfx.Screen; import javafx.scene.canvas.Canvas; @@ -12,362 +9,382 @@ import javafx.scene.paint.Paint; import javafx.scene.shape.StrokeLineCap; import javafx.scene.shape.StrokeLineJoin; +import java.util.ArrayList; +import java.util.List; + public class TurtlePainter implements IPaintLayer, ITurtle { - static class TurtleState { - protected Point pos; - protected Direction dir; - protected Direction inDir; - protected double penSize = 1.0; - protected Paint ink = Color.BLACK; + static class TurtleState { + protected Point pos; + protected Direction dir; + protected Direction inDir; + protected double penSize = 1.0; + protected Paint ink = Color.BLACK; - public TurtleState() { - } + public TurtleState() { + } - public TurtleState(TurtleState s) { - pos = s.pos; - dir = s.dir; - inDir = s.inDir; - penSize = s.penSize; - ink = s.ink; - } - } + public TurtleState(TurtleState s) { + pos = s.pos; + dir = s.dir; + inDir = s.inDir; + penSize = s.penSize; + ink = s.ink; + } + } - private final Screen screen; - private final double width; - private final double height; - private final GraphicsContext context; - private final List stateStack = new ArrayList<>(); + private final Screen screen; + private final double width; + private final double height; + private final GraphicsContext context; + private final List stateStack = new ArrayList<>(); - private TurtleState state = new TurtleState(); - private final Canvas canvas; - private boolean path = false; + private TurtleState state = new TurtleState(); + private final Canvas canvas; + private boolean path = false; - public TurtlePainter(double width, double height) { - this.screen = null; - this.canvas = null; - this.context = null; - this.width = width; - this.height = height; - stateStack.add(new TurtleState()); - state.dir = new Direction(1.0, 0.0); - state.pos = new Point(width / 2, height / 2); - } + public TurtlePainter(double width, double height) { + this.screen = null; + this.canvas = null; + this.context = null; + this.width = width; + this.height = height; + stateStack.add(new TurtleState()); + state.dir = new Direction(1.0, 0.0); + state.pos = new Point(width / 2, height / 2); + } - public TurtlePainter(Screen screen, Canvas canvas) { - if (screen == null || canvas == null) - throw new IllegalArgumentException(); - this.screen = screen; - this.canvas = canvas; - this.context = canvas.getGraphicsContext2D(); - this.width = screen.getWidth(); - this.height = screen.getHeight(); - stateStack.add(new TurtleState()); - state.dir = new Direction(1.0, 0.0); - state.pos = new Point(screen.getWidth() / 2, screen.getHeight() / 2); - context.setLineJoin(StrokeLineJoin.BEVEL); - context.setLineCap(StrokeLineCap.SQUARE); - } + public TurtlePainter(Screen screen, Canvas canvas) { + if (screen == null || canvas == null) { + throw new IllegalArgumentException(); + } + this.screen = screen; + this.canvas = canvas; + this.context = canvas.getGraphicsContext2D(); + this.width = screen.getWidth(); + this.height = screen.getHeight(); + stateStack.add(new TurtleState()); + state.dir = new Direction(1.0, 0.0); + state.pos = new Point(screen.getWidth() / 2, screen.getHeight() / 2); + context.setLineJoin(StrokeLineJoin.BEVEL); + context.setLineCap(StrokeLineCap.SQUARE); + } - @Override - @SuppressWarnings("unchecked") - public T as(Class clazz) { - if (clazz == GraphicsContext.class) - return (T) context; - if (clazz == getClass()) - return (T) this; - else - return null; - } + @Override + @SuppressWarnings("unchecked") + public T as(Class clazz) { + if (clazz == GraphicsContext.class) { + return (T) context; + } + if (clazz == getClass()) { + return (T) this; + } else { + return null; + } + } - @Override - public void clear() { - if (context != null) - context.clearRect(0, 0, getWidth(), getHeight()); - } + @Override + public void clear() { + if (context != null) { + context.clearRect(0, 0, getWidth(), getHeight()); + } + } - @Override - public ITurtle curveTo(Point to, double startControl, double endAngle, double endControl) { - Point c1 = state.pos.move(state.dir, startControl); - Point c2 = to.move(Direction.fromDegrees(endAngle + 180), endControl); - if (context != null) { - if (!path) { - // context.save(); - context.setStroke(state.ink); - context.setLineWidth(state.penSize); - context.beginPath(); - context.moveTo(state.pos.getX(), state.pos.getY()); - } - context.bezierCurveTo(c1.getX(), c1.getY(), c2.getX(), c2.getY(), to.getX(), to.getY()); - } - state.inDir = state.dir; - state.pos = to; - state.dir = Direction.fromDegrees(endAngle); + @Override + public ITurtle curveTo(Point to, double startControl, double endAngle, double endControl) { + Point c1 = state.pos.move(state.dir, startControl); + Point c2 = to.move(Direction.fromDegrees(endAngle + 180), endControl); + if (context != null) { + if (!path) { + // context.save(); + context.setStroke(state.ink); + context.setLineWidth(state.penSize); + context.beginPath(); + context.moveTo(state.pos.getX(), state.pos.getY()); + } + context.bezierCurveTo(c1.getX(), c1.getY(), c2.getX(), c2.getY(), to.getX(), to.getY()); + } + state.inDir = state.dir; + state.pos = to; + state.dir = Direction.fromDegrees(endAngle); - if (!path && context != null) { - context.stroke(); - // context.restore(); - } - return this; - } + if (!path && context != null) { + context.stroke(); + // context.restore(); + } + return this; + } - @Override - public void debugTurtle() { - System.err.println("[" + state.pos + " " + state.dir + "]"); - } + @Override + public void debugTurtle() { + System.err.println("[" + state.pos + " " + state.dir + "]"); + } - @Override - public ITurtle draw(double dist) { - Point to = state.pos.move(state.dir, dist); - return drawTo(to); - } + @Override + public ITurtle draw(double dist) { + Point to = state.pos.move(state.dir, dist); + return drawTo(to); + } - @Override - public ITurtle draw(Point relPos) { - Point to = state.pos.move(relPos); - return drawTo(to); - } + @Override + public ITurtle draw(Point relPos) { + Point to = state.pos.move(relPos); + return drawTo(to); + } - @Override - public ITurtle drawTo(double x, double y) { - Point to = new Point(x, y); - return drawTo(to); - } + @Override + public ITurtle drawTo(double x, double y) { + Point to = new Point(x, y); + return drawTo(to); + } - @Override - public ITurtle drawTo(Point to) { - if (path && context != null) { - context.setStroke(state.ink); - context.setLineWidth(state.penSize); - context.lineTo(to.getX(), to.getY()); - } else { - line(to); - } - state.inDir = state.dir; - state.pos = to; - return this; - } + @Override + public ITurtle drawTo(Point to) { + if (path && context != null) { + context.setStroke(state.ink); + context.setLineWidth(state.penSize); + context.lineTo(to.getX(), to.getY()); + } else { + line(to); + } + state.inDir = state.dir; + state.pos = to; + return this; + } - @Override - public double getAngle() { - return state.dir.toDegrees(); - } + @Override + public double getAngle() { + return state.dir.toDegrees(); + } - @Override - public Direction getDirection() { - return state.dir; - } + @Override + public Direction getDirection() { + return state.dir; + } - @Override - public double getHeight() { - return height; - } + @Override + public double getHeight() { + return height; + } - @Override - public Point getPos() { - return state.pos; - } + @Override + public Point getPos() { + return state.pos; + } - public Screen getScreen() { - return screen; - } + public Screen getScreen() { + return screen; + } - @Override - public double getWidth() { - return width; - } + @Override + public double getWidth() { + return width; + } - @Override - public ITurtle jump(double dist) { - state.inDir = state.dir; - state.pos = state.pos.move(state.dir, dist); - if (path && context != null) - context.moveTo(state.pos.getX(), state.pos.getY()); - return this; - } + @Override + public ITurtle jump(double dist) { + state.inDir = state.dir; + state.pos = state.pos.move(state.dir, dist); + if (path && context != null) { + context.moveTo(state.pos.getX(), state.pos.getY()); + } + return this; + } - @Override - public ITurtle jump(Point relPos) { - // TODO: state.inDir = state.dir; - state.pos = state.pos.move(relPos); - if (path && context != null) - context.moveTo(state.pos.getX(), state.pos.getY()); + @Override + public ITurtle jump(Point relPos) { + // TODO: state.inDir = state.dir; + state.pos = state.pos.move(relPos); + if (path && context != null) { + context.moveTo(state.pos.getX(), state.pos.getY()); + } - return this; - } + return this; + } - @Override - public ITurtle jumpTo(double x, double y) { - state.inDir = state.dir; - state.pos = new Point(x, y); - return this; - } + @Override + public ITurtle jumpTo(double x, double y) { + state.inDir = state.dir; + state.pos = new Point(x, y); + return this; + } - @Override - public ITurtle jumpTo(Point to) { - state.inDir = state.dir; - state.pos = to; - return this; - } + @Override + public ITurtle jumpTo(Point to) { + state.inDir = state.dir; + state.pos = to; + return this; + } - @Override - public void layerToBack() { - if (screen != null) - screen.moveToBack(this); - } + @Override + public void layerToBack() { + if (screen != null) { + screen.moveToBack(this); + } + } - @Override - public void layerToFront() { - if (screen != null) - screen.moveToFront(this); - } + @Override + public void layerToFront() { + if (screen != null) { + screen.moveToFront(this); + } + } - @Override - public ITurtle line(Point to) { - if (context != null) { - // context.save(); - context.setStroke(state.ink); - context.setLineWidth(state.penSize); - context.strokeLine(state.pos.getX(), state.pos.getY(), to.getX(), to.getY()); - // context.restore(); - } - return this; - } + @Override + public ITurtle line(Point to) { + if (context != null) { + // context.save(); + context.setStroke(state.ink); + context.setLineWidth(state.penSize); + context.strokeLine(state.pos.getX(), state.pos.getY(), to.getX(), to.getY()); + // context.restore(); + } + return this; + } - @Override - public IPainter restore() { - if (stateStack.size() > 0) { - state = stateStack.remove(stateStack.size() - 1); - } - return this; - } + @Override + public IPainter restore() { + if (stateStack.size() > 0) { + state = stateStack.remove(stateStack.size() - 1); + } + return this; + } - @Override - public IPainter save() { - stateStack.add(new TurtleState(state)); - return this; - } + @Override + public IPainter save() { + stateStack.add(new TurtleState(state)); + return this; + } - @Override - public IPainter setInk(Paint ink) { - state.ink = ink; - return this; - } + @Override + public IPainter setInk(Paint ink) { + state.ink = ink; + return this; + } - @Override - public ITurtle setPenSize(double pixels) { - if (pixels < 0) - throw new IllegalArgumentException("Negative: " + pixels); - state.penSize = pixels; - return this; - } + @Override + public ITurtle setPenSize(double pixels) { + if (pixels < 0) { + throw new IllegalArgumentException("Negative: " + pixels); + } + state.penSize = pixels; + return this; + } - @Override - public IShape shape() { - ShapePainter s = new ShapePainter(context); - return s.at(getPos()).rotation(getAngle()).strokePaint(state.ink); - } + @Override + public IShape shape() { + ShapePainter s = new ShapePainter(context); + return s.at(getPos()).rotation(getAngle()).strokePaint(state.ink); + } - @Override - public ITurtle turn(double degrees) { - state.dir = state.dir.turn(degrees); - return this; - } + @Override + public ITurtle turn(double degrees) { + state.dir = state.dir.turn(degrees); + return this; + } - @Override - public ITurtle turnAround() { - return turn(180); - } + @Override + public ITurtle turnAround() { + return turn(180); + } - @Override - public ITurtle turnLeft() { - return turn(90); - } + @Override + public ITurtle turnLeft() { + return turn(90); + } - @Override - public ITurtle turnLeft(double degrees) { - if (degrees < 0) - throw new IllegalArgumentException("Negative: " + degrees + " (use turn())"); - state.dir = state.dir.turn(degrees); - return this; - } + @Override + public ITurtle turnLeft(double degrees) { + if (degrees < 0) { + throw new IllegalArgumentException("Negative: " + degrees + " (use turn())"); + } + state.dir = state.dir.turn(degrees); + return this; + } - @Override - public ITurtle turnRight() { - return turn(-90); - } + @Override + public ITurtle turnRight() { + return turn(-90); + } - @Override - public ITurtle turnRight(double degrees) { - if (degrees < 0) - throw new IllegalArgumentException("Negative: " + degrees + " (use turn())"); - state.dir = state.dir.turn(-degrees); - return this; - } + @Override + public ITurtle turnRight(double degrees) { + if (degrees < 0) { + throw new IllegalArgumentException("Negative: " + degrees + " (use turn())"); + } + state.dir = state.dir.turn(-degrees); + return this; + } - @Override - public ITurtle turnTo(double degrees) { - state.dir = state.dir.turnTo(degrees); - return this; - } + @Override + public ITurtle turnTo(double degrees) { + state.dir = state.dir.turnTo(degrees); + return this; + } - @Override - public ITurtle turnTowards(double degrees, double percent) { - state.dir = state.dir.turnTowards(new Direction(degrees), percent); - return this; - } + @Override + public ITurtle turnTowards(double degrees, double percent) { + state.dir = state.dir.turnTowards(new Direction(degrees), percent); + return this; + } - @Override - public ITurtle turtle() { - TurtlePainter painter = screen != null ? new TurtlePainter(screen, canvas) : new TurtlePainter(width, height); - painter.stateStack.set(0, new TurtleState(state)); - return painter; - } + @Override + public ITurtle turtle() { + TurtlePainter painter = screen != null ? new TurtlePainter(screen, canvas) : new TurtlePainter(width, height); + painter.stateStack.set(0, new TurtleState(state)); + return painter; + } - public ITurtle beginPath() { - if (path) - throw new IllegalStateException("beginPath() after beginPath()"); - path = true; - if (context != null) { - context.setStroke(state.ink); - context.beginPath(); - context.moveTo(state.pos.getX(), state.pos.getY()); - } - return this; - } + public ITurtle beginPath() { + if (path) { + throw new IllegalStateException("beginPath() after beginPath()"); + } + path = true; + if (context != null) { + context.setStroke(state.ink); + context.beginPath(); + context.moveTo(state.pos.getX(), state.pos.getY()); + } + return this; + } - public ITurtle closePath() { - if (!path) - throw new IllegalStateException("closePath() without beginPath()"); - if (context != null) - context.closePath(); - return this; - } + public ITurtle closePath() { + if (!path) { + throw new IllegalStateException("closePath() without beginPath()"); + } + if (context != null) { + context.closePath(); + } + return this; + } - public ITurtle endPath() { - if (!path) - throw new IllegalStateException("endPath() without beginPath()"); - path = false; - if (context != null) - context.stroke(); - return this; - } + public ITurtle endPath() { + if (!path) { + throw new IllegalStateException("endPath() without beginPath()"); + } + path = false; + if (context != null) { + context.stroke(); + } + return this; + } - public ITurtle fillPath() { - if (!path) - throw new IllegalStateException("fillPath() without beginPath()"); - path = false; - if (context != null) { - context.save(); - context.setFill(state.ink); - context.fill(); - context.restore(); - } - return this; - } + public ITurtle fillPath() { + if (!path) { + throw new IllegalStateException("fillPath() without beginPath()"); + } + path = false; + if (context != null) { + context.save(); + context.setFill(state.ink); + context.fill(); + context.restore(); + } + return this; + } - @Override - public Paint getInk() { - return state.ink; - } + @Override + public Paint getInk() { + return state.ink; + } } diff --git a/src/main/java/inf101/v18/gfx/textmode/BlocksAndBoxes.java b/src/main/java/inf101/v18/gfx/textmode/BlocksAndBoxes.java index 0f8f975..5566347 100644 --- a/src/main/java/inf101/v18/gfx/textmode/BlocksAndBoxes.java +++ b/src/main/java/inf101/v18/gfx/textmode/BlocksAndBoxes.java @@ -6,196 +6,200 @@ import java.util.List; import java.util.function.BiFunction; public class BlocksAndBoxes { - public enum PixelOrder implements Iterable { - LEFT_TO_RIGHT(8, 4, 2, 1), RIGHT_TO_LEFT(4, 8, 1, 2), LEFT_TO_RIGHT_UPWARDS(2, 1, 8, - 4), RIGHT_TO_LEFT_UPWARDS(1, 2, 4, 8); + public enum PixelOrder implements Iterable { + LEFT_TO_RIGHT(8, 4, 2, 1), RIGHT_TO_LEFT(4, 8, 1, 2), LEFT_TO_RIGHT_UPWARDS(2, 1, 8, + 4), RIGHT_TO_LEFT_UPWARDS(1, 2, 4, 8); - private List order; + private List order; - PixelOrder(int a, int b, int c, int d) { - order = Arrays.asList(a, b, c, d); - } + PixelOrder(int a, int b, int c, int d) { + order = Arrays.asList(a, b, c, d); + } - @Override - public Iterator iterator() { - return order.iterator(); - } - } + @Override + public Iterator iterator() { + return order.iterator(); + } + } - public static final String[] unicodeBlocks = { " ", "▗", "▖", "▄", "▝", "▐", "▞", "▟", "▘", "▚", "▌", "▙", "▀", "▜", - "▛", "█", "▒" }; + public static final String[] unicodeBlocks = {" ", "▗", "▖", "▄", "▝", "▐", "▞", "▟", "▘", "▚", "▌", "▙", "▀", "▜", + "▛", "█", "▒"}; - public static final int[] unicodeBlocks_NumPixels = { 0, 1, 1, 2, 1, 2, 2, 3, 1, 2, 2, 3, 2, 3, 3, 4, 2 }; - public static final String unicodeBlocksString = String.join("", unicodeBlocks); - public static final String BLOCK_EMPTY = " "; - public static final String BLOCK_BOTTOM_RIGHT = "▗"; - public static final String BLOCK_BOTTOM_LEFT = "▖"; - public static final String BLOCK_BOTTOM = "▄"; - public static final String BLOCK_TOP_RIGHT = "▝"; - public static final String BLOCK_RIGHT = "▐"; - public static final String BLOCK_DIAG_FORWARD = "▞"; - public static final String BLOCK_REVERSE_TOP_LEFT = "▟"; - public static final String BLOCK_TOP_LEFT = "▘"; - public static final String BLOCK_DIAG_BACKWARD = "▚"; - public static final String BLOCK_LEFT = "▌"; - public static final String BLOCK_REVERSE_TOP_RIGHT = "▙"; - public static final String BLOCK_TOP = "▀"; - public static final String BLOCK_REVERSE_BOTTOM_LEFT = "▜"; - public static final String BLOCK_REVERSE_BOTTOM_RIGHT = "▛"; - public static final String BLOCK_FULL = "█"; + public static final int[] unicodeBlocks_NumPixels = {0, 1, 1, 2, 1, 2, 2, 3, 1, 2, 2, 3, 2, 3, 3, 4, 2}; + public static final String unicodeBlocksString = String.join("", unicodeBlocks); + public static final String BLOCK_EMPTY = " "; + public static final String BLOCK_BOTTOM_RIGHT = "▗"; + public static final String BLOCK_BOTTOM_LEFT = "▖"; + public static final String BLOCK_BOTTOM = "▄"; + public static final String BLOCK_TOP_RIGHT = "▝"; + public static final String BLOCK_RIGHT = "▐"; + public static final String BLOCK_DIAG_FORWARD = "▞"; + public static final String BLOCK_REVERSE_TOP_LEFT = "▟"; + public static final String BLOCK_TOP_LEFT = "▘"; + public static final String BLOCK_DIAG_BACKWARD = "▚"; + public static final String BLOCK_LEFT = "▌"; + public static final String BLOCK_REVERSE_TOP_RIGHT = "▙"; + public static final String BLOCK_TOP = "▀"; + public static final String BLOCK_REVERSE_BOTTOM_LEFT = "▜"; + public static final String BLOCK_REVERSE_BOTTOM_RIGHT = "▛"; + public static final String BLOCK_FULL = "█"; - public static final String BLOCK_HALF = "▒"; + public static final String BLOCK_HALF = "▒"; - public static String blockAddOne(String s, PixelOrder order) { - int i = BlocksAndBoxes.unicodeBlocksString.indexOf(s); - if (i >= 0) { - for (int bit : order) { - if ((i & bit) == 0) - return unicodeBlocks[i | bit]; - } - } - return s; - } + public static String blockAddOne(String s, PixelOrder order) { + int i = BlocksAndBoxes.unicodeBlocksString.indexOf(s); + if (i >= 0) { + for (int bit : order) { + if ((i & bit) == 0) { + return unicodeBlocks[i | bit]; + } + } + } + return s; + } - /** - * Convert a string into a Unicode block graphics character. - * - *

    - * The block characters corresponds to 2x2-pixel images, and can be used, e.g., - * to draw a 80x44 pixel image on a 40x22 character text screen. - * - *

    - * The blocks are specified by four-character strings of spaces and asterisks, - * with space indicating an open space and asterisk indicating a filled "pixel", - * with the pixels arranged in a left-to-right, top-to-bottom order: - * - *

    -	 * 01
    -	 * 23
    -	 * 
    - * - *

    - * So "* **" corresponds to the block character "▚", - * with this layout: - * - *

    -	 * * 
    -	 *  *
    -	 * 
    - * - *

    - * The special codes "++++" and "+" corresponds to the - * "grey" block "▒", and "*" corresponds to the - * "black" block "█". - * - * @param s - * A four character string, indicating which block character to - * select. - * @return A Unicode block character - * @throws IllegalArgumentException - * if the string isn't of the expected form - */ - public static String blockChar(String s) { - switch (s.replaceAll("\n", s)) { - case " ": - return unicodeBlocks[0]; - case " *": - return unicodeBlocks[1]; - case " * ": - return unicodeBlocks[2]; - case " **": - return unicodeBlocks[3]; - case " * ": - return unicodeBlocks[4]; - case " * *": - return unicodeBlocks[5]; - case " ** ": - return unicodeBlocks[6]; - case " ***": - return unicodeBlocks[7]; - case "* ": - return unicodeBlocks[8]; - case "* *": - return unicodeBlocks[9]; - case "* * ": - return unicodeBlocks[10]; - case "* **": - return unicodeBlocks[11]; - case "** ": - return unicodeBlocks[12]; - case "** *": - return unicodeBlocks[13]; - case "*** ": - return unicodeBlocks[14]; - case "****": - return unicodeBlocks[15]; - case "++++": - return unicodeBlocks[16]; - case ".": - return BLOCK_BOTTOM_LEFT; - case "_": - return BLOCK_BOTTOM; - case "/": - return BLOCK_DIAG_FORWARD; - case "\\": - return BLOCK_DIAG_BACKWARD; - case "|": - return BLOCK_LEFT; - case "#": - return BLOCK_FULL; - case "`": - return BLOCK_TOP_LEFT; - case "'": - return BLOCK_TOP_RIGHT; - } - throw new IllegalArgumentException( - "Expected length 4 string of \" \" and \"*\", or \"++++\", got \"" + s + "\""); - } + /** + * Convert a string into a Unicode block graphics character. + * + *

    + * The block characters corresponds to 2x2-pixel images, and can be used, e.g., + * to draw a 80x44 pixel image on a 40x22 character text screen. + * + *

    + * The blocks are specified by four-character strings of spaces and asterisks, + * with space indicating an open space and asterisk indicating a filled "pixel", + * with the pixels arranged in a left-to-right, top-to-bottom order: + * + *

    +     * 01
    +     * 23
    +     * 
    + * + *

    + * So "* **" corresponds to the block character "▚", + * with this layout: + * + *

    +     * *
    +     *  *
    +     * 
    + * + *

    + * The special codes "++++" and "+" corresponds to the + * "grey" block "▒", and "*" corresponds to the + * "black" block "█". + * + * @param s A four character string, indicating which block character to + * select. + * @return A Unicode block character + * @throws IllegalArgumentException if the string isn't of the expected form + */ + public static String blockChar(String s) { + switch (s.replaceAll("\n", s)) { + case " ": + return unicodeBlocks[0]; + case " *": + return unicodeBlocks[1]; + case " * ": + return unicodeBlocks[2]; + case " **": + return unicodeBlocks[3]; + case " * ": + return unicodeBlocks[4]; + case " * *": + return unicodeBlocks[5]; + case " ** ": + return unicodeBlocks[6]; + case " ***": + return unicodeBlocks[7]; + case "* ": + return unicodeBlocks[8]; + case "* *": + return unicodeBlocks[9]; + case "* * ": + return unicodeBlocks[10]; + case "* **": + return unicodeBlocks[11]; + case "** ": + return unicodeBlocks[12]; + case "** *": + return unicodeBlocks[13]; + case "*** ": + return unicodeBlocks[14]; + case "****": + return unicodeBlocks[15]; + case "++++": + return unicodeBlocks[16]; + case ".": + return BLOCK_BOTTOM_LEFT; + case "_": + return BLOCK_BOTTOM; + case "/": + return BLOCK_DIAG_FORWARD; + case "\\": + return BLOCK_DIAG_BACKWARD; + case "|": + return BLOCK_LEFT; + case "#": + return BLOCK_FULL; + case "`": + return BLOCK_TOP_LEFT; + case "'": + return BLOCK_TOP_RIGHT; + } + throw new IllegalArgumentException( + "Expected length 4 string of \" \" and \"*\", or \"++++\", got \"" + s + "\""); + } - public static String blockCompact(String s) { - int i = BlocksAndBoxes.unicodeBlocksString.indexOf(s); - if (i > 0) { - int lower = i & 3; - int upper = (i >> 2) & 3; - i = (lower | upper) | ((lower & upper) << 2); - // System.out.println("Compact: " + s + " -> " + BlocksAndBoxes.unicodeBlocks[i] - // + "\n"); - return BlocksAndBoxes.unicodeBlocks[i]; - } - return s; - } + public static String blockCompact(String s) { + int i = BlocksAndBoxes.unicodeBlocksString.indexOf(s); + if (i > 0) { + int lower = i & 3; + int upper = (i >> 2) & 3; + i = (lower | upper) | ((lower & upper) << 2); + // System.out.println("Compact: " + s + " -> " + BlocksAndBoxes.unicodeBlocks[i] + // + "\n"); + return BlocksAndBoxes.unicodeBlocks[i]; + } + return s; + } - public static String blockCompose(String b1, String b2, BiFunction op) { - int i1 = unicodeBlocksString.indexOf(b1); - if (i1 < 0) - throw new IllegalArgumentException("Not a block character: " + b1); - int i2 = unicodeBlocksString.indexOf(b2); - if (i2 < 0) - throw new IllegalArgumentException("Not a block character: " + b1); - if (i1 == 16 || i2 == 16) - return b2; - else - return unicodeBlocks[op.apply(i1, i2)]; - } + public static String blockCompose(String b1, String b2, BiFunction op) { + int i1 = unicodeBlocksString.indexOf(b1); + if (i1 < 0) { + throw new IllegalArgumentException("Not a block character: " + b1); + } + int i2 = unicodeBlocksString.indexOf(b2); + if (i2 < 0) { + throw new IllegalArgumentException("Not a block character: " + b1); + } + if (i1 == 16 || i2 == 16) { + return b2; + } else { + return unicodeBlocks[op.apply(i1, i2)]; + } + } - public static String blockComposeOrOverwrite(String b1, String b2, BiFunction op) { - int i1 = unicodeBlocksString.indexOf(b1); - int i2 = unicodeBlocksString.indexOf(b2); - if (i1 < 0 || i2 < 0 || i1 == 16 || i2 == 16) - return b2; - else - return unicodeBlocks[op.apply(i1, i2)]; - } + public static String blockComposeOrOverwrite(String b1, String b2, BiFunction op) { + int i1 = unicodeBlocksString.indexOf(b1); + int i2 = unicodeBlocksString.indexOf(b2); + if (i1 < 0 || i2 < 0 || i1 == 16 || i2 == 16) { + return b2; + } else { + return unicodeBlocks[op.apply(i1, i2)]; + } + } - public static String blockRemoveOne(String s, PixelOrder order) { - int i = BlocksAndBoxes.unicodeBlocksString.indexOf(s); - if (i >= 0) { - for (int bit : order) { - if ((i & bit) != 0) - return unicodeBlocks[i & ~bit]; - } - } - return s; - } + public static String blockRemoveOne(String s, PixelOrder order) { + int i = BlocksAndBoxes.unicodeBlocksString.indexOf(s); + if (i >= 0) { + for (int bit : order) { + if ((i & bit) != 0) { + return unicodeBlocks[i & ~bit]; + } + } + } + return s; + } } diff --git a/src/main/java/inf101/v18/gfx/textmode/ControlSequences.java b/src/main/java/inf101/v18/gfx/textmode/ControlSequences.java index 6f76793..914793a 100644 --- a/src/main/java/inf101/v18/gfx/textmode/ControlSequences.java +++ b/src/main/java/inf101/v18/gfx/textmode/ControlSequences.java @@ -1,5 +1,7 @@ package inf101.v18.gfx.textmode; +import javafx.scene.paint.Color; + import java.util.ArrayList; import java.util.HashMap; import java.util.Iterator; @@ -11,266 +13,271 @@ import java.util.function.Consumer; import java.util.regex.Matcher; import java.util.regex.Pattern; -import javafx.scene.paint.Color; - public class ControlSequences { - private static final boolean DEBUG = false; - - public static class CsiPattern { - public static CsiPattern compile0(String pat, String desc, Consumer handler) { - CsiPattern csiPattern = new CsiPattern(pat, 0, 0, desc, handler, null, null); - patterns.put(csiPattern.getCommandLetter(), csiPattern); - return csiPattern; - } + private static final boolean DEBUG = false; - public static CsiPattern compile1(String pat, int defaultArg, String desc, - BiConsumer handler) { - CsiPattern csiPattern = new CsiPattern(pat, defaultArg, 1, desc, null, handler, null); - patterns.put(csiPattern.getCommandLetter(), csiPattern); - return csiPattern; - } + public static class CsiPattern { + public static CsiPattern compile0(String pat, String desc, Consumer handler) { + CsiPattern csiPattern = new CsiPattern(pat, 0, 0, desc, handler, null, null); + patterns.put(csiPattern.getCommandLetter(), csiPattern); + return csiPattern; + } - public static CsiPattern compileN(String pat, int defaultArg, int numArgs, String desc, - BiConsumer> handler) { - CsiPattern csiPattern = new CsiPattern(pat, defaultArg, numArgs, desc, null, null, handler); - patterns.put(csiPattern.getCommandLetter(), csiPattern); - return csiPattern; - } + public static CsiPattern compile1(String pat, int defaultArg, String desc, + BiConsumer handler) { + CsiPattern csiPattern = new CsiPattern(pat, defaultArg, 1, desc, null, handler, null); + patterns.put(csiPattern.getCommandLetter(), csiPattern); + return csiPattern; + } - private String patStr; - private Pattern pattern; - private int defaultArg = 0; - private String desc; - private Consumer handler0; + public static CsiPattern compileN(String pat, int defaultArg, int numArgs, String desc, + BiConsumer> handler) { + CsiPattern csiPattern = new CsiPattern(pat, defaultArg, numArgs, desc, null, null, handler); + patterns.put(csiPattern.getCommandLetter(), csiPattern); + return csiPattern; + } - private BiConsumer handler1; + private String patStr; + private Pattern pattern; + private int defaultArg = 0; + private String desc; + private Consumer handler0; - private BiConsumer> handlerN; + private BiConsumer handler1; - private int numArgs; + private BiConsumer> handlerN; - public CsiPattern(String pat, int defaultArg, int numArgs, String desc, Consumer handler0, - BiConsumer handler1, BiConsumer> handlerN) { - this.patStr = pat; - this.pattern = Pattern.compile(pat); - this.defaultArg = defaultArg; - this.numArgs = numArgs; - this.desc = desc; - this.handler0 = handler0; - this.handler1 = handler1; - this.handlerN = handlerN; - } + private int numArgs; - public String getCommandLetter() { - return patStr.substring(patStr.length() - 1); - } + public CsiPattern(String pat, int defaultArg, int numArgs, String desc, Consumer handler0, + BiConsumer handler1, BiConsumer> handlerN) { + this.patStr = pat; + this.pattern = Pattern.compile(pat); + this.defaultArg = defaultArg; + this.numArgs = numArgs; + this.desc = desc; + this.handler0 = handler0; + this.handler1 = handler1; + this.handlerN = handlerN; + } - public String getDescription() { - return desc; - } + public String getCommandLetter() { + return patStr.substring(patStr.length() - 1); + } - public boolean match(Printer printer, String input) { - Matcher matcher = pattern.matcher(input); - if (matcher.matches()) { - String argStr = matcher.groupCount() > 0 ? matcher.group(1) : ""; - String[] args = argStr.split(";"); - if (handler0 != null) { - if(DEBUG) - System.out.println("Handling " + getDescription() + "."); - handler0.accept(printer); - } else if (handler1 != null) { - int arg = args.length > 0 && !args[0].equals("") ? Integer.valueOf(args[0]) : defaultArg; - if(DEBUG) - System.out.println("Handling " + getDescription() + ": " + arg); - handler1.accept(printer, arg); - } else if (handlerN != null) { - List argList = new ArrayList<>(); - for (String s : args) { - if (s.equals("")) - argList.add(defaultArg); - else - argList.add(Integer.valueOf(s)); - } - while (argList.size() < numArgs) { - argList.add(defaultArg); - } - if(DEBUG) -System.out.println("Handling " + getDescription() + ": " + argList); - handlerN.accept(printer, argList); - } - return true; - } - return false; - } - } + public String getDescription() { + return desc; + } - public static final Map patterns = new HashMap<>(); - public static final CsiPattern CUU = CsiPattern.compile1("\u001b\\\u005b([0-9;]*)A", 1, "cursor up", - (Printer p, Integer i) -> { - p.move(0, -i); - }); - public static final CsiPattern CUD = CsiPattern.compile1("\u001b\\\u005b([0-9;]*)B", 1, "cursor down", - (Printer p, Integer i) -> { - p.move(0, i); - }); - public static final CsiPattern CUF = CsiPattern.compile1("\u001b\\\u005b([0-9;]*)C", 1, "cursor forward", - (Printer p, Integer i) -> { - p.move(i, 0); - }); - public static final CsiPattern CUB = CsiPattern.compile1("\u001b\\\u005b([0-9;]*)D", 1, "cursor back", - (Printer p, Integer i) -> { - p.move(-i, 0); - }); - public static final CsiPattern CNL = CsiPattern.compile1("\u001b\\\u005b([0-9;]*)E", 1, "cursor next line", - (Printer p, Integer i) -> { - p.move(0, i); - p.beginningOfLine(); - }); - public static final CsiPattern CPL = CsiPattern.compile1("\u001b\\\u005b([0-9;]*)F", 1, "cursor previous line", - (Printer p, Integer i) -> { - p.move(0, -i); - p.beginningOfLine(); - }); - public static final CsiPattern CHA = CsiPattern.compile1("\u001b\\\u005b([0-9;]*)G", 1, - "cursor horizontal absolute", (Printer p, Integer i) -> { - p.moveTo(i, p.getY()); - }); - public static final CsiPattern CUP = CsiPattern.compileN("\u001b\\\u005b([0-9;]*)H", 1, 2, "cursor position", - (Printer p, List i) -> { - p.moveTo(i.get(1), i.get(0)); - }); - public static final CsiPattern ED = CsiPattern.compile1("\u001b\\\u005b([0-9;]*)J", 0, "erase in display", - (Printer p, Integer i) -> { - if (i == 2 || i == 3) - p.clear(); - else - System.err.println("Unimplemented: ED"); - }); - public static final CsiPattern EK = CsiPattern.compile1("\u001b\\\u005b([0-9;]*)K", 0, "erase in line", - (Printer p, Integer i) -> { - System.err.println("Unimplemented: EK"); - }); - public static final CsiPattern SU = CsiPattern.compile1("\u001b\\\u005b([0-9;]*)S", 1, "scroll up", - (Printer p, Integer i) -> { - p.scroll(i); - }); - public static final CsiPattern SD = CsiPattern.compile1("\u001b\\\u005b([0-9;]*)T", 1, "scroll down", - (Printer p, Integer i) -> { - p.scroll(-i); - }); - public static final CsiPattern HVP = CsiPattern.compileN("\u001b\\\u005b([0-9;]*)f", 1, 2, - "horizontal vertical position", (Printer p, List l) -> { - p.moveTo(l.get(1), l.get(0)); - }); - public static final CsiPattern AUX_ON = CsiPattern.compile0("\u001b\\\u005b5i", "aux port on", (Printer p) -> { - System.err.println("Unimplemented: AUX on"); - }); - public static final CsiPattern AUX_OFF = CsiPattern.compile0("\u001b\\\u005b4i", "aux port off", (Printer p) -> { - System.err.println("Unimplemented: AUX off"); - }); - public static final CsiPattern DSR = CsiPattern.compile0("\u001b\\\u005b6n", "device status report", - (Printer p) -> { - System.out.println("ESC[" + p.getY() + ";" + p.getX() + "R"); - }); - public static final CsiPattern SCP = CsiPattern.compile0("\u001b\\\u005bs", "save cursor position", (Printer p) -> { - p.saveCursor(); - }); - public static final CsiPattern RCP = CsiPattern.compile0("\u001b\\\u005bu", "restore cursor position", - (Printer p) -> { - p.restoreCursor(); - }); - public static final int F = 0xFF, H = 0xAA, L = 0x55, OFF = 0x00; - public static final Color[] PALETTE_CGA = { // - Color.rgb(0, 0, 0), Color.rgb(0, 0, H), Color.rgb(0, H, 0), Color.rgb(0, H, H), // - Color.rgb(H, 0, 0), Color.rgb(H, 0, H), Color.rgb(H, L, 0), Color.rgb(H, H, H), // - Color.rgb(L, L, L), Color.rgb(L, L, F), Color.rgb(L, F, L), Color.rgb(L, F, F), // - Color.rgb(F, L, L), Color.rgb(F, L, F), Color.rgb(F, F, L), Color.rgb(F, F, F), }; - public static final Color[] PALETTE_VGA = { // - Color.rgb(0, 0, 0), Color.rgb(H, 0, 0), Color.rgb(0, H, 0), Color.rgb(H, H, 0), // - Color.rgb(0, 0, H), Color.rgb(H, 0, H), Color.rgb(0, H, H), Color.rgb(H, H, H), // - Color.rgb(L, L, L), Color.rgb(F, L, L), Color.rgb(L, F, L), Color.rgb(F, F, L), // - Color.rgb(L, L, F), Color.rgb(F, L, F), Color.rgb(L, F, F), Color.rgb(F, F, F), }; + public boolean match(Printer printer, String input) { + Matcher matcher = pattern.matcher(input); + if (matcher.matches()) { + String argStr = matcher.groupCount() > 0 ? matcher.group(1) : ""; + String[] args = argStr.split(";"); + if (handler0 != null) { + if (DEBUG) { + System.out.println("Handling " + getDescription() + "."); + } + handler0.accept(printer); + } else if (handler1 != null) { + int arg = args.length > 0 && !args[0].equals("") ? Integer.valueOf(args[0]) : defaultArg; + if (DEBUG) { + System.out.println("Handling " + getDescription() + ": " + arg); + } + handler1.accept(printer, arg); + } else if (handlerN != null) { + List argList = new ArrayList<>(); + for (String s : args) { + if (s.equals("")) { + argList.add(defaultArg); + } else { + argList.add(Integer.valueOf(s)); + } + } + while (argList.size() < numArgs) { + argList.add(defaultArg); + } + if (DEBUG) { + System.out.println("Handling " + getDescription() + ": " + argList); + } + handlerN.accept(printer, argList); + } + return true; + } + return false; + } + } - public static final CsiPattern SGR = CsiPattern.compileN("\u001b\\\u005b([0-9;]*)m", 0, -1, - "select graphics rendition", (Printer p, List l) -> { - if (l.size() == 0) { - l.add(0); - } - int[] attrs = { 0, TextFont.ATTR_BRIGHT, TextFont.ATTR_FAINT, TextFont.ATTR_ITALIC, - TextFont.ATTR_UNDERLINE, TextFont.ATTR_BLINK, TextFont.ATTR_BLINK, TextFont.ATTR_INVERSE, 0, - TextFont.ATTR_LINE_THROUGH }; + public static final Map patterns = new HashMap<>(); + public static final CsiPattern CUU = CsiPattern.compile1("\u001b\\\u005b([0-9;]*)A", 1, "cursor up", + (Printer p, Integer i) -> { + p.move(0, -i); + }); + public static final CsiPattern CUD = CsiPattern.compile1("\u001b\\\u005b([0-9;]*)B", 1, "cursor down", + (Printer p, Integer i) -> { + p.move(0, i); + }); + public static final CsiPattern CUF = CsiPattern.compile1("\u001b\\\u005b([0-9;]*)C", 1, "cursor forward", + (Printer p, Integer i) -> { + p.move(i, 0); + }); + public static final CsiPattern CUB = CsiPattern.compile1("\u001b\\\u005b([0-9;]*)D", 1, "cursor back", + (Printer p, Integer i) -> { + p.move(-i, 0); + }); + public static final CsiPattern CNL = CsiPattern.compile1("\u001b\\\u005b([0-9;]*)E", 1, "cursor next line", + (Printer p, Integer i) -> { + p.move(0, i); + p.beginningOfLine(); + }); + public static final CsiPattern CPL = CsiPattern.compile1("\u001b\\\u005b([0-9;]*)F", 1, "cursor previous line", + (Printer p, Integer i) -> { + p.move(0, -i); + p.beginningOfLine(); + }); + public static final CsiPattern CHA = CsiPattern.compile1("\u001b\\\u005b([0-9;]*)G", 1, + "cursor horizontal absolute", (Printer p, Integer i) -> { + p.moveTo(i, p.getY()); + }); + public static final CsiPattern CUP = CsiPattern.compileN("\u001b\\\u005b([0-9;]*)H", 1, 2, "cursor position", + (Printer p, List i) -> { + p.moveTo(i.get(1), i.get(0)); + }); + public static final CsiPattern ED = CsiPattern.compile1("\u001b\\\u005b([0-9;]*)J", 0, "erase in display", + (Printer p, Integer i) -> { + if (i == 2 || i == 3) { + p.clear(); + } else { + System.err.println("Unimplemented: ED"); + } + }); + public static final CsiPattern EK = CsiPattern.compile1("\u001b\\\u005b([0-9;]*)K", 0, "erase in line", + (Printer p, Integer i) -> { + System.err.println("Unimplemented: EK"); + }); + public static final CsiPattern SU = CsiPattern.compile1("\u001b\\\u005b([0-9;]*)S", 1, "scroll up", + (Printer p, Integer i) -> { + p.scroll(i); + }); + public static final CsiPattern SD = CsiPattern.compile1("\u001b\\\u005b([0-9;]*)T", 1, "scroll down", + (Printer p, Integer i) -> { + p.scroll(-i); + }); + public static final CsiPattern HVP = CsiPattern.compileN("\u001b\\\u005b([0-9;]*)f", 1, 2, + "horizontal vertical position", (Printer p, List l) -> { + p.moveTo(l.get(1), l.get(0)); + }); + public static final CsiPattern AUX_ON = CsiPattern.compile0("\u001b\\\u005b5i", "aux port on", (Printer p) -> { + System.err.println("Unimplemented: AUX on"); + }); + public static final CsiPattern AUX_OFF = CsiPattern.compile0("\u001b\\\u005b4i", "aux port off", (Printer p) -> { + System.err.println("Unimplemented: AUX off"); + }); + public static final CsiPattern DSR = CsiPattern.compile0("\u001b\\\u005b6n", "device status report", + (Printer p) -> { + System.out.println("ESC[" + p.getY() + ";" + p.getX() + "R"); + }); + public static final CsiPattern SCP = CsiPattern.compile0("\u001b\\\u005bs", "save cursor position", (Printer p) -> { + p.saveCursor(); + }); + public static final CsiPattern RCP = CsiPattern.compile0("\u001b\\\u005bu", "restore cursor position", + (Printer p) -> { + p.restoreCursor(); + }); + public static final int F = 0xFF, H = 0xAA, L = 0x55, OFF = 0x00; + public static final Color[] PALETTE_CGA = { // + Color.rgb(0, 0, 0), Color.rgb(0, 0, H), Color.rgb(0, H, 0), Color.rgb(0, H, H), // + Color.rgb(H, 0, 0), Color.rgb(H, 0, H), Color.rgb(H, L, 0), Color.rgb(H, H, H), // + Color.rgb(L, L, L), Color.rgb(L, L, F), Color.rgb(L, F, L), Color.rgb(L, F, F), // + Color.rgb(F, L, L), Color.rgb(F, L, F), Color.rgb(F, F, L), Color.rgb(F, F, F),}; + public static final Color[] PALETTE_VGA = { // + Color.rgb(0, 0, 0), Color.rgb(H, 0, 0), Color.rgb(0, H, 0), Color.rgb(H, H, 0), // + Color.rgb(0, 0, H), Color.rgb(H, 0, H), Color.rgb(0, H, H), Color.rgb(H, H, H), // + Color.rgb(L, L, L), Color.rgb(F, L, L), Color.rgb(L, F, L), Color.rgb(F, F, L), // + Color.rgb(L, L, F), Color.rgb(F, L, F), Color.rgb(L, F, F), Color.rgb(F, F, F),}; - Iterator it = l.iterator(); - while (it.hasNext()) { - int i = it.next(); - if (i == 0) { - p.setVideoAttrs(0); - p.setInk(PALETTE_VGA[7]); - p.setBackground(PALETTE_VGA[0]); - } else if (i < 10) { - p.setVideoAttrEnabled(attrs[i]); - } else if (i >= 20 && i < 30) { - p.setVideoAttrDisabled(attrs[i] - 20); - } else if (i >= 30 && i < 38) { - p.setInk(PALETTE_VGA[i - 30]); - } else if (i == 38) { - p.setInk(decode256(it)); - } else if (i == 29) { - p.setInk(Color.WHITE); - } else if (i >= 40 && i < 48) { - p.setBackground(PALETTE_VGA[i - 40]); - } else if (i == 48) { - p.setInk(decode256(it)); - } else if (i == 49) { - p.setBackground(Color.BLACK); - } else if (i >= 90 && i < 98) { - p.setInk(PALETTE_VGA[8 + i - 90]); - } else if (i >= 100 && i < 108) { - p.setBackground(PALETTE_VGA[8 + i - 100]); - } else if (i == 53) { - p.setVideoAttrEnabled(TextFont.ATTR_OVERLINE); - } else if (i == 55) { - p.setVideoAttrEnabled(TextFont.ATTR_OVERLINE); - } - } - }); + public static final CsiPattern SGR = CsiPattern.compileN("\u001b\\\u005b([0-9;]*)m", 0, -1, + "select graphics rendition", (Printer p, List l) -> { + if (l.size() == 0) { + l.add(0); + } + int[] attrs = {0, TextFont.ATTR_BRIGHT, TextFont.ATTR_FAINT, TextFont.ATTR_ITALIC, + TextFont.ATTR_UNDERLINE, TextFont.ATTR_BLINK, TextFont.ATTR_BLINK, TextFont.ATTR_INVERSE, 0, + TextFont.ATTR_LINE_THROUGH}; - public static boolean applyCsi(Printer printer, String csi) { - CsiPattern csiPattern = patterns.get(csi.substring(csi.length() - 1)); - // System.out.println("Applying CSI: " + csi.replaceAll("\u001b", "ESC")); + Iterator it = l.iterator(); + while (it.hasNext()) { + int i = it.next(); + if (i == 0) { + p.setVideoAttrs(0); + p.setInk(PALETTE_VGA[7]); + p.setBackground(PALETTE_VGA[0]); + } else if (i < 10) { + p.setVideoAttrEnabled(attrs[i]); + } else if (i >= 20 && i < 30) { + p.setVideoAttrDisabled(attrs[i] - 20); + } else if (i >= 30 && i < 38) { + p.setInk(PALETTE_VGA[i - 30]); + } else if (i == 38) { + p.setInk(decode256(it)); + } else if (i == 29) { + p.setInk(Color.WHITE); + } else if (i >= 40 && i < 48) { + p.setBackground(PALETTE_VGA[i - 40]); + } else if (i == 48) { + p.setInk(decode256(it)); + } else if (i == 49) { + p.setBackground(Color.BLACK); + } else if (i >= 90 && i < 98) { + p.setInk(PALETTE_VGA[8 + i - 90]); + } else if (i >= 100 && i < 108) { + p.setBackground(PALETTE_VGA[8 + i - 100]); + } else if (i == 53) { + p.setVideoAttrEnabled(TextFont.ATTR_OVERLINE); + } else if (i == 55) { + p.setVideoAttrEnabled(TextFont.ATTR_OVERLINE); + } + } + }); - if (csiPattern != null) { - if (csiPattern.match(printer, csi)) - return true; - else - System.err.println("Handler failed for escape sequence: " + csi.replaceAll("\u001b", "ESC")); + public static boolean applyCsi(Printer printer, String csi) { + CsiPattern csiPattern = patterns.get(csi.substring(csi.length() - 1)); + // System.out.println("Applying CSI: " + csi.replaceAll("\u001b", "ESC")); - } else { - System.err.println("No handler for escape sequence: " + csi.replaceAll("\u001b", "ESC")); - } - return false; - } + if (csiPattern != null) { + if (csiPattern.match(printer, csi)) { + return true; + } else { + System.err.println("Handler failed for escape sequence: " + csi.replaceAll("\u001b", "ESC")); + } - private static Color decode256(Iterator it) { - int i; - try { - i = it.next(); - if (i == 5) { - i = it.next(); - if (i < 16) - return PALETTE_VGA[i]; - else if (i < 232) - return Color.rgb(i / 36, (i / 6) % 6, i % 6); - else - return Color.gray((i - 232) / 23.0); - } else if (i == 2) { - int r = it.next(); - int g = it.next(); - int b = it.next(); - return Color.rgb(r, g, b); - } - } catch (NoSuchElementException e) { - } - return null; - } + } else { + System.err.println("No handler for escape sequence: " + csi.replaceAll("\u001b", "ESC")); + } + return false; + } + + private static Color decode256(Iterator it) { + int i; + try { + i = it.next(); + if (i == 5) { + i = it.next(); + if (i < 16) { + return PALETTE_VGA[i]; + } else if (i < 232) { + return Color.rgb(i / 36, (i / 6) % 6, i % 6); + } else { + return Color.gray((i - 232) / 23.0); + } + } else if (i == 2) { + int r = it.next(); + int g = it.next(); + int b = it.next(); + return Color.rgb(r, g, b); + } + } catch (NoSuchElementException e) { + } + return null; + } } diff --git a/src/main/java/inf101/v18/gfx/textmode/DemoPages.java b/src/main/java/inf101/v18/gfx/textmode/DemoPages.java index 935480c..5f2a7e2 100644 --- a/src/main/java/inf101/v18/gfx/textmode/DemoPages.java +++ b/src/main/java/inf101/v18/gfx/textmode/DemoPages.java @@ -1,153 +1,161 @@ package inf101.v18.gfx.textmode; +import javafx.scene.paint.Color; + import java.io.BufferedReader; import java.io.IOException; import java.io.InputStream; import java.io.InputStreamReader; -import javafx.scene.paint.Color; - public class DemoPages { - public static void printAnsiArt(Printer printer) { - printer.moveTo(1, 1); - printer.setAutoScroll(false); - printer.clear(); + public static void printAnsiArt(Printer printer) { + printer.moveTo(1, 1); + printer.setAutoScroll(false); + printer.clear(); - try (InputStream stream = DemoPages.class.getResourceAsStream("flower.txt")) { - BufferedReader reader = new BufferedReader(new InputStreamReader(stream, "UTF-8")); - for (String s = reader.readLine(); s != null; s = reader.readLine()) { - printer.println(s); - } - } catch (IOException e) { - e.printStackTrace(); - } + try (InputStream stream = DemoPages.class.getResourceAsStream("flower.txt")) { + BufferedReader reader = new BufferedReader(new InputStreamReader(stream, "UTF-8")); + for (String s = reader.readLine(); s != null; s = reader.readLine()) { + printer.println(s); + } + } catch (IOException e) { + e.printStackTrace(); + } - } + } - public static void printBlockPlotting(Printer printer) { - printer.clear(); - printer.setAutoScroll(false); - printer.setVideoAttrs(0); - int topLine = 8; - for (int x = 0; x < 16; x += 1) { - if ((x & 1) > 0) - printer.plot(4 * x + 1, 1 + (topLine - 1) * 2); - if ((x & 2) > 0) - printer.plot(4 * x, 1 + (topLine - 1) * 2); - if ((x & 4) > 0) - printer.plot(4 * x + 1, (topLine - 1) * 2); - if ((x & 8) > 0) - printer.plot(4 * x, (topLine - 1) * 2); - printer.printAt(1 + 2 * x, topLine + 2, BlocksAndBoxes.unicodeBlocks[x]); - printer.printAt(1 + 2 * x, topLine + 4, BlocksAndBoxes.unicodeBlocks[15]); - printer.printAt(1 + 2 * x, topLine + 6, BlocksAndBoxes.unicodeBlocks[~x & +0xf]); - printer.printAt(1 + 2 * x, topLine + 7, String.format("%X", x)); - if ((x & 1) > 0) - printer.unplot(4 * x + 1, 1 + (4 + topLine - 1) * 2); - if ((x & 2) > 0) - printer.unplot(4 * x, 1 + (4 + topLine - 1) * 2); - if ((x & 4) > 0) - printer.unplot(4 * x + 1, (4 + topLine - 1) * 2); - if ((x & 8) > 0) - printer.unplot(4 * x, (4 + topLine - 1) * 2); - } - printer.printAt(1, 1, - "Plotting with Unicode Block Elements\n(ZX81-like Graphics)\n\nThe plot/print and unplot/inverse\nlines should be equal:"); - printer.printAt(33, topLine, "plot"); - printer.printAt(33, topLine + 2, "print"); - printer.printAt(33, topLine + 4, "unplot"); - printer.printAt(33, topLine + 6, "inverse"); - printer.printAt(0, topLine + 9, String.format("Full blocks:\n Clear[%s] Shaded[%s] Opaque[%s]", - BlocksAndBoxes.unicodeBlocks[0], BlocksAndBoxes.unicodeBlocks[16], BlocksAndBoxes.unicodeBlocks[15])); - printer.printAt(41, topLine + 9, "(ZX81 inverted shade and half block"); - printer.printAt(41, topLine + 10, "shades are missing in Unicode and"); - printer.printAt(41, topLine + 11, "therefore not supported)"); - printer.println(); - } + public static void printBlockPlotting(Printer printer) { + printer.clear(); + printer.setAutoScroll(false); + printer.setVideoAttrs(0); + int topLine = 8; + for (int x = 0; x < 16; x += 1) { + if ((x & 1) > 0) { + printer.plot(4 * x + 1, 1 + (topLine - 1) * 2); + } + if ((x & 2) > 0) { + printer.plot(4 * x, 1 + (topLine - 1) * 2); + } + if ((x & 4) > 0) { + printer.plot(4 * x + 1, (topLine - 1) * 2); + } + if ((x & 8) > 0) { + printer.plot(4 * x, (topLine - 1) * 2); + } + printer.printAt(1 + 2 * x, topLine + 2, BlocksAndBoxes.unicodeBlocks[x]); + printer.printAt(1 + 2 * x, topLine + 4, BlocksAndBoxes.unicodeBlocks[15]); + printer.printAt(1 + 2 * x, topLine + 6, BlocksAndBoxes.unicodeBlocks[~x & +0xf]); + printer.printAt(1 + 2 * x, topLine + 7, String.format("%X", x)); + if ((x & 1) > 0) { + printer.unplot(4 * x + 1, 1 + (4 + topLine - 1) * 2); + } + if ((x & 2) > 0) { + printer.unplot(4 * x, 1 + (4 + topLine - 1) * 2); + } + if ((x & 4) > 0) { + printer.unplot(4 * x + 1, (4 + topLine - 1) * 2); + } + if ((x & 8) > 0) { + printer.unplot(4 * x, (4 + topLine - 1) * 2); + } + } + printer.printAt(1, 1, + "Plotting with Unicode Block Elements\n(ZX81-like Graphics)\n\nThe plot/print and unplot/inverse\nlines should be equal:"); + printer.printAt(33, topLine, "plot"); + printer.printAt(33, topLine + 2, "print"); + printer.printAt(33, topLine + 4, "unplot"); + printer.printAt(33, topLine + 6, "inverse"); + printer.printAt(0, topLine + 9, String.format("Full blocks:\n Clear[%s] Shaded[%s] Opaque[%s]", + BlocksAndBoxes.unicodeBlocks[0], BlocksAndBoxes.unicodeBlocks[16], BlocksAndBoxes.unicodeBlocks[15])); + printer.printAt(41, topLine + 9, "(ZX81 inverted shade and half block"); + printer.printAt(41, topLine + 10, "shades are missing in Unicode and"); + printer.printAt(41, topLine + 11, "therefore not supported)"); + printer.println(); + } - public static void printBoxDrawing(Printer printer) { - printer.clear(); - printer.setAutoScroll(false); - printer.println(" Latin-1 Boxes & Blocks"); - printer.println(" U+0000..00FF U+2500..257F..259F"); - printer.println(" "); - printer.println(" 0123456789ABCDEF 0123456789ABCDEF"); - for (int y = 0; y < 16; y++) { - printer.print(String.format(" %X", y)); - int c = y * 0x010; - for (int x = 0; x < 16; x++) { - printer.print(c >= 0x20 ? Character.toString((char) (c + x)) : " "); - } - printer.print(" "); + public static void printBoxDrawing(Printer printer) { + printer.clear(); + printer.setAutoScroll(false); + printer.println(" Latin-1 Boxes & Blocks"); + printer.println(" U+0000..00FF U+2500..257F..259F"); + printer.println(" "); + printer.println(" 0123456789ABCDEF 0123456789ABCDEF"); + for (int y = 0; y < 16; y++) { + printer.print(String.format(" %X", y)); + int c = y * 0x010; + for (int x = 0; x < 16; x++) { + printer.print(c >= 0x20 ? Character.toString((char) (c + x)) : " "); + } + printer.print(" "); - if (y < 10) { - printer.print(String.format("%X", y)); - c = 0x2500 + y * 0x010; - for (int x = 0; x < 16; x++) { - printer.print(Character.toString((char) (c + x))); - } - } - printer.println(); - } - } + if (y < 10) { + printer.print(String.format("%X", y)); + c = 0x2500 + y * 0x010; + for (int x = 0; x < 16; x++) { + printer.print(Character.toString((char) (c + x))); + } + } + printer.println(); + } + } - public static void printVideoAttributes(Printer printer) { - printer.clear(); - printer.setAutoScroll(false); - printer.setVideoAttrs(0); - printer.setInk(Color.BLACK); - printer.setStroke(Color.WHITE); + public static void printVideoAttributes(Printer printer) { + printer.clear(); + printer.setAutoScroll(false); + printer.setVideoAttrs(0); + printer.setInk(Color.BLACK); + printer.setStroke(Color.WHITE); - String demoLine = "Lorem=ipsum-dolor$sit.ametÆØÅå*,|▞&Jumps Over\\the?fLat Dog{}()#\"!"; - printer.println("RIBU|" + demoLine); - for (int i = 1; i < 16; i++) { - printer.setVideoAttrs(i); - String s = (i & 1) != 0 ? "X" : " "; - s += (i & 2) != 0 ? "X" : " "; - s += (i & 4) != 0 ? "X" : " "; - s += (i & 8) != 0 ? "X" : " "; - printer.println(s + "|" + demoLine); - } - printer.setVideoAttrs(0); - printer.println(); - printer.println("Lines: under, through, over"); - printer.setVideoAttrs(TextFont.ATTR_UNDERLINE); - printer.println(" " + demoLine + " "); - printer.setVideoAttrs(TextFont.ATTR_LINE_THROUGH); - printer.println(" " + demoLine + " "); - printer.setVideoAttrs(TextFont.ATTR_OVERLINE); - printer.println(" " + demoLine + " "); - printer.setVideoAttrs(0); + String demoLine = "Lorem=ipsum-dolor$sit.ametÆØÅå*,|▞&Jumps Over\\the?fLat Dog{}()#\"!"; + printer.println("RIBU|" + demoLine); + for (int i = 1; i < 16; i++) { + printer.setVideoAttrs(i); + String s = (i & 1) != 0 ? "X" : " "; + s += (i & 2) != 0 ? "X" : " "; + s += (i & 4) != 0 ? "X" : " "; + s += (i & 8) != 0 ? "X" : " "; + printer.println(s + "|" + demoLine); + } + printer.setVideoAttrs(0); + printer.println(); + printer.println("Lines: under, through, over"); + printer.setVideoAttrs(TextFont.ATTR_UNDERLINE); + printer.println(" " + demoLine + " "); + printer.setVideoAttrs(TextFont.ATTR_LINE_THROUGH); + printer.println(" " + demoLine + " "); + printer.setVideoAttrs(TextFont.ATTR_OVERLINE); + printer.println(" " + demoLine + " "); + printer.setVideoAttrs(0); - } + } - public static void printZX(Printer printer) { - printer.moveTo(1, 1); - printer.setAutoScroll(false); - printer.clear(); - printer.println(" ▄▄▄ ▄ ▄ ▄"); - printer.println(" █ █ █ █ █ █ █"); - printer.println(" █ █ █ █ █ █ █"); - printer.println(" █ █ █ █ █ █ █"); - printer.println(" ▀ ▀ ▀ ▀ ▀▀▀"); - printer.println(" ▄▄▄ ▄▄"); - printer.println(" █ █"); - printer.println(" █ █"); - printer.println(" █ █"); - printer.println(" ▀▀▀ ▀▀"); - printer.println(" ▄▄ ▄ ▄ ▄"); - printer.println(" █ █ █ █ █ █"); - printer.println(" █ █ █ █ █ █"); - printer.println(" █ █ █ █ █ █"); - printer.println(" ▀▀ ▀ ▀ ▀▀▀"); - printer.println("ON █████ █ █ ███ █"); - printer.println("THE █ █ █ █ █ ██"); - printer.println("SINCLAIR █ ███ █"); - printer.println(" █ █ █ █ █ WITH"); - printer.println(" █ █ █ █ █ █ 16K"); - printer.println(" █████ █ █ ███ ███ RAM"); - printer.moveTo(1, 1); - printer.setAutoScroll(true); - } + public static void printZX(Printer printer) { + printer.moveTo(1, 1); + printer.setAutoScroll(false); + printer.clear(); + printer.println(" ▄▄▄ ▄ ▄ ▄"); + printer.println(" █ █ █ █ █ █ █"); + printer.println(" █ █ █ █ █ █ █"); + printer.println(" █ █ █ █ █ █ █"); + printer.println(" ▀ ▀ ▀ ▀ ▀▀▀"); + printer.println(" ▄▄▄ ▄▄"); + printer.println(" █ █"); + printer.println(" █ █"); + printer.println(" █ █"); + printer.println(" ▀▀▀ ▀▀"); + printer.println(" ▄▄ ▄ ▄ ▄"); + printer.println(" █ █ █ █ █ █"); + printer.println(" █ █ █ █ █ █"); + printer.println(" █ █ █ █ █ █"); + printer.println(" ▀▀ ▀ ▀ ▀▀▀"); + printer.println("ON █████ █ █ ███ █"); + printer.println("THE █ █ █ █ █ ██"); + printer.println("SINCLAIR █ ███ █"); + printer.println(" █ █ █ █ █ WITH"); + printer.println(" █ █ █ █ █ █ 16K"); + printer.println(" █████ █ █ ███ ███ RAM"); + printer.moveTo(1, 1); + printer.setAutoScroll(true); + } } diff --git a/src/main/java/inf101/v18/gfx/textmode/Printer.java b/src/main/java/inf101/v18/gfx/textmode/Printer.java index f095d9b..c170fb5 100644 --- a/src/main/java/inf101/v18/gfx/textmode/Printer.java +++ b/src/main/java/inf101/v18/gfx/textmode/Printer.java @@ -1,10 +1,5 @@ package inf101.v18.gfx.textmode; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.List; -import java.util.function.BiFunction; - import inf101.v18.gfx.IPaintLayer; import inf101.v18.gfx.Screen; import javafx.scene.canvas.Canvas; @@ -13,832 +8,861 @@ import javafx.scene.effect.BlendMode; import javafx.scene.paint.Color; import javafx.scene.paint.Paint; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.function.BiFunction; + public class Printer implements IPaintLayer { - private static class Char { - public int mode; - public String s; - public Color fill; - public Color stroke; - public Paint bg; - - public Char(String s, Color fill, Color stroke, Paint bg, int mode) { - this.s = s; - this.fill = fill; - this.stroke = stroke; - this.bg = bg; - this.mode = mode; - } - } - - public static final TextFont FONT_MONOSPACED = new TextFont("Monospaced", 27.00, TextMode.CHAR_BOX_SIZE, 3.4000, - -6.7000, 1.5000, 1.0000, true); - public static final TextFont FONT_LMMONO = new TextFont("lmmono10-regular.otf", 30.00, TextMode.CHAR_BOX_SIZE, - 4.0000, -8.5000, 1.5000, 1.0000, true); - public static final TextFont FONT_ZXSPECTRUM7 = new TextFont("ZXSpectrum-7.otf", 22.00, TextMode.CHAR_BOX_SIZE, - 3.1000, -3.8000, 1.0000, 1.0000, true); - - /** - * TTF file can be found here: http://users.teilar.gr/~g1951d/ in this ZIP file: - * http://users.teilar.gr/~g1951d/Symbola.zip - *

    - * (Put the extracted Symbola.ttf in src/inf101/v18/gfx/fonts/) - */ - public static final TextFont FONT_SYMBOLA = new TextFont("Symbola.ttf", 26.70, TextMode.CHAR_BOX_SIZE, -0.4000, - -7.6000, 1.35000, 1.0000, true); - - /** - * TTF file can be found here: - * http://www.kreativekorp.com/software/fonts/c64.shtml - */ - public static final TextFont FONT_GIANA = new TextFont("Giana.ttf", 25.00, TextMode.CHAR_BOX_SIZE, 4.6000, -5.0000, - 1.0000, 1.0000, true); - - /** - * TTF file can be found here: - * http://www.kreativekorp.com/software/fonts/c64.shtml - */ - public static final TextFont FONT_C64 = new TextFont("PetMe64.ttf", 31.50, TextMode.CHAR_BOX_SIZE, 0.0000, -4.000, - 1.0000, 1.0000, true); - private static final Paint DEFAULT_BACKGROUND = Color.TRANSPARENT; - private static final TextMode DEFAULT_MODE = TextMode.MODE_40X22; - private static final boolean DEBUG_REDRAW = false; - - public static String center(String s, int width) { - for (; s.length() < width; s = " " + s + " ") - ; - return s; - } - - public static String repeat(String s, int width) { - String r = s; - for (; r.length() < width; r += s) - ; - return r; - } - - Color DEFAULT_FILL = Color.WHITE; - - Color DEFAULT_STROKE = Color.TRANSPARENT; - - private TextMode textMode; - private Color fill; - private Color stroke; - private Paint background; - private Screen screen; - private List lineBuffer = new ArrayList<>(); - private boolean autoscroll = true; - private final Canvas textPage; - private int x = 1, y = 1, savedX = 1, savedY = 1; - // private int pageWidth = LINE_WIDTHS[resMode], pageHeight = - // PAGE_HEIGHTS[resMode]; - private int leftMargin = 1, topMargin = 1; - private TextFont font = FONT_SYMBOLA; - private int videoAttrs = 0; - - private String csiSeq = null; - - private boolean csiEnabled = true; - - private int csiMode = 0; - - private final double width; - private final double height; - - private int dirtyX0 = Integer.MAX_VALUE; - private int dirtyX1 = Integer.MIN_VALUE; - private int dirtyY0 = Integer.MAX_VALUE; - private int dirtyY1 = Integer.MIN_VALUE; - private boolean useBuffer = true; - - public Printer(double width, double height) { - this.screen = null; - this.textPage = null; - this.width = width; - this.height = height; - for (int i = 0; i < TextMode.PAGE_HEIGHT_MAX; i++) { - lineBuffer.add(new Char[TextMode.LINE_WIDTH_MAX]); - } - resetFull(); - } - - public Printer(Screen screen, Canvas page) { - this.screen = screen; - this.textPage = page; - this.width = page.getWidth(); - this.height = page.getHeight(); - for (int i = 0; i < TextMode.PAGE_HEIGHT_MAX; i++) { - lineBuffer.add(new Char[TextMode.LINE_WIDTH_MAX]); - } - resetFull(); - } - - public void addToCharBuffer(String string) { - string.codePoints().mapToObj((int i) -> String.valueOf(Character.toChars(i))).forEach((String s) -> { - if (csiMode != 0) { - s = addToCsiBuffer(s); - } - switch (s) { - case "\r": - moveTo(leftMargin, y); - break; - case "\n": - moveTo(leftMargin, y + 1); - break; - case "\f": - moveTo(leftMargin, topMargin); - for (Char[] line : lineBuffer) - Arrays.fill(line, null); - if(textPage != null) { - GraphicsContext context = textPage.getGraphicsContext2D(); - if (background != null && background != Color.TRANSPARENT) { - context.setFill(background); - context.fillRect(0.0, 0.0, textPage.getWidth(), textPage.getHeight()); - } else - context.clearRect(0.0, 0.0, textPage.getWidth(), textPage.getHeight()); - } - break; - case "\b": - moveHoriz(-1); - break; - case "\t": - moveTo((x + 8) % 8, y); - break; - case "\u001b": - if (csiEnabled) { - csiSeq = s; - csiMode = 1; - } - break; - default: - if (s.length() > 0 && s.codePointAt(0) >= 0x20) { - drawChar(x, y, setChar(x, y, s)); - moveHoriz(1); - } - break; - } - }); - } - - private String addToCsiBuffer(String s) { - if (csiMode == 1) { - switch (s) { - case "[": - csiMode = 2; - csiSeq += s; - break; - case "c": - csiMode = 0; - resetFull(); - break; - default: - csiReset(); - return s; - } - } else if (csiMode == 2) { - int c = s.codePointAt(0); - if (c >= 0x30 && c <= 0x3f) { - csiSeq += s; - } else if (c >= 0x20 && c <= 0x2f) { - csiMode = 3; - csiSeq += s; - } else if (c >= 0x40 && c <= 0x7e) { - csiSeq += s; - csiFinish(); - } else { - csiReset(); - return s; - } - - } else if (csiMode == 3) { - int c = s.codePointAt(0); - if (c >= 0x20 && c <= 0x2f) { - csiSeq += s; - } else if (c >= 0x40 && c <= 0x7e) { - csiSeq += s; - csiFinish(); - } else { - csiReset(); - return s; - } - } - return ""; - } - - public void beginningOfLine() { - x = leftMargin; - } - - public void beginningOfPage() { - x = leftMargin; - y = topMargin; - } - - @Override - public void clear() { - print("\f"); - } - - public void clearAt(int x, int y) { - printAt(x, y, " "); - } - - public void clearLine(int y) { - y = constrainY(y); - if (y > 0 && y <= TextMode.PAGE_HEIGHT_MAX) { - Arrays.fill(lineBuffer.get(y - 1), null); - dirty(1, y); - dirty(getLineWidth(), y); - if (!useBuffer) - redrawDirty(); - } - } - - public void clearRegion(int x, int y, int width, int height) { - if (x > getLineWidth() || y > getPageHeight()) - return; - int x2 = Math.min(x + width - 1, getLineWidth()); - int y2 = Math.min(y + height - 1, getPageHeight()); - if (x2 < 1 || y2 < 1) - return; - int x1 = Math.max(1, x); - int y1 = Math.max(1, y); - // Char fillWith = new Char("*", Color.BLACK, Color.GREEN, Color.TRANSPARENT, - // 0); - for (int i = y1; i <= y2; i++) { - Arrays.fill(lineBuffer.get(i - 1), x1 - 1, x2, null); - } - dirty(x1, y1); - dirty(x2, y2); - if (!useBuffer) - redrawDirty(); - } - - private int constrainX(int x) { - return x; // Math.min(LINE_WIDTH_HIRES, Math.max(1, x)); - } - - public int constrainY(int y) { - return y; // Math.min(pageHeight, Math.max(1, y)); - } - - public int constrainYOrScroll(int y) { - if (autoscroll) { - if (y < 1) { - scroll(y - 1); - return 1; - } else if (y > getPageHeight()) { - scroll(y - getPageHeight()); - return getPageHeight(); - } - } - - return y;// Math.min(pageHeight, Math.max(1, y)); - } - - private void csiFinish() { - ControlSequences.applyCsi(this, csiSeq); - csiReset(); - } - - private void csiReset() { - csiMode = 0; - csiSeq = null; - } - - public void cycleMode(boolean adjustDisplayAspect) { - textMode = textMode.nextMode(); - if (adjustDisplayAspect && screen != null) - screen.setAspect(textMode.getAspect()); - dirty(1, 1); - dirty(getLineWidth(), getPageHeight()); - if (!useBuffer) - redrawDirty(); - } - - private void drawChar(int x, int y, Char c) { - if (useBuffer) { - dirty(x, y); - } else if (c != null && textPage != null) { - GraphicsContext context = textPage.getGraphicsContext2D(); - - context.setFill(c.fill); - context.setStroke(c.stroke); - font.drawTextAt(context, (x - 1) * getCharWidth(), y * getCharHeight(), c.s, - textMode.getCharWidth() / textMode.getCharBoxSize(), c.mode, c.bg); - } - } - - public void drawCharCells() { - if (screen != null) { - GraphicsContext context = screen.getBackgroundContext(); - screen.clearBackground(); - double w = getCharWidth(); - double h = getCharHeight(); - context.save(); - context.setGlobalBlendMode(BlendMode.EXCLUSION); - context.setFill(Color.WHITE.deriveColor(0.0, 1.0, 1.0, 0.1)); - for (int x = 0; x < getLineWidth(); x++) { - for (int y = 0; y < getPageHeight(); y++) { - if ((x + y) % 2 == 0) - context.fillRect(x * w, y * h, w, h); - } - } - context.restore(); - } - } - - public Color getBackground(int x, int y) { - Char c = null; - if (x > 0 && x <= TextMode.LINE_WIDTH_MAX && y > 0 && y <= TextMode.PAGE_HEIGHT_MAX) { - c = lineBuffer.get(y - 1)[x - 1]; - } - Color bg = Color.TRANSPARENT; - if (c != null && c.bg instanceof Color) - bg = (Color) c.bg; - else if (background instanceof Color) - bg = (Color) background; - return bg; - } - - public boolean getBold() { - return (videoAttrs & TextFont.ATTR_BRIGHT) != 0; - } - - public String getChar(int x, int y) { - Char c = null; - if (x > 0 && x <= TextMode.LINE_WIDTH_MAX && y > 0 && y <= TextMode.PAGE_HEIGHT_MAX) { - c = lineBuffer.get(y - 1)[x - 1]; - } - if (c != null) - return c.s; - else - return " "; - } - - public double getCharHeight() { - return textMode.getCharHeight(); - } - - public double getCharWidth() { - return textMode.getCharWidth(); - } - - public Color getColor(int x, int y) { - Char c = null; - if (x > 0 && x <= TextMode.LINE_WIDTH_MAX && y > 0 && y <= TextMode.PAGE_HEIGHT_MAX) { - c = lineBuffer.get(y - 1)[x - 1]; - } - if (c != null) - return c.fill; - else - return fill; - } - - public TextFont getFont() { - return font; - } - - public boolean getItalics() { - return (videoAttrs & TextFont.ATTR_ITALIC) != 0; - } - - /** - * @return the leftMargin - */ - public int getLeftMargin() { - return leftMargin; - } - - public int getLineWidth() { - return textMode.getLineWidth(); - } - - public int getPageHeight() { - return textMode.getPageHeight(); - } - - public boolean getReverseVideo() { - return (videoAttrs & TextFont.ATTR_INVERSE) != 0; - } - - public TextMode getTextMode() { - return textMode; - } - - /** - * @return the topMargin - */ - public int getTopMargin() { - return topMargin; - } - - public int getVideoMode() { - return videoAttrs; - } - - public int getX() { - return x; - } - - public int getY() { - return y; - } - - public boolean isFilled(int x, int y) { - return !getChar(x, y).equals(" "); - } - - @Override - public void layerToBack() { - if (screen != null) { - screen.moveToBack(this); - } - } - - @Override - public void layerToFront() { - if (screen != null) { - screen.moveToFront(this); - } - } - - public void move(int deltaX, int deltaY) { - x = constrainX(x + deltaX); - y = constrainYOrScroll(y + deltaY); - } - - public void moveHoriz(int dist) { - x = constrainX(x + dist); - } - - public void moveTo(int newX, int newY) { - x = constrainX(newX); - y = constrainYOrScroll(newY); - } - - public void moveVert(int dist) { - y = constrainYOrScroll(y + dist); - } - - public void plot(int x, int y) { - plot(x, y, (a, b) -> a | b); - } - - public void plot(int x, int y, BiFunction op) { - int textX = (x) / 2 + 1; - int textY = (y) / 2 + 1; - int bitPos = (x + 1) % 2 + ((y + 1) % 2) * 2; - String blockChar = BlocksAndBoxes.unicodeBlocks[1 << bitPos]; - // System.out.println(blockChar + ", " + bitPos + ", ("+ (x) + ", " + (y) + ")"+ - // ", (" + (textX) + ", " + (textY) + ")"); - String s = BlocksAndBoxes.blockComposeOrOverwrite(getChar(textX, textY), blockChar, op); - // System.out.println("Merge '" + getChar(textX, textY) + "' + '" + blockChar + - // "' = '" + s + "'"); - printAt(textX, textY, s); - } - - public void print(String s) { - addToCharBuffer(s); - } - - public void print(String s, Color paint) { - Color tmp = fill; - fill = paint; - addToCharBuffer(s); - fill = tmp; - } - - public void printAt(int atX, int atY, String s) { - moveTo(atX, atY); - print(s); - } - - public void printAt(int atX, int atY, String s, Color ink) { - moveTo(atX, atY); - print(s, ink); - } - - public void println() { - print("\n"); - } - - public void println(String s) { - print(s); - print("\n"); - } - - public void redrawTextPage() { - redrawTextPage(1, 1, getLineWidth(), getPageHeight()); - clean(); - } - - private void redrawTextPage(int x0, int y0, int x1, int y1) { - /* - * System.out.printf("redrawTextPage benchmark"); - * System.out.printf(" %5s %5s %7s %4s %5s %5s %5s%n", "ms", "chars", - * "ms/char", "mode", "indir", "inv", "fake"); for (int m = -1; m < 8; m++) { - * long t0 = System.currentTimeMillis(); int n = 0; - */ - if(textPage == null) - return; - GraphicsContext context = textPage.getGraphicsContext2D(); - - double px0 = (x0 - 1) * getCharWidth(), py0 = (y0 - 1) * getCharHeight(); - double px1 = x1 * getCharWidth(), py1 = y1 * getCharHeight(); - if (DEBUG_REDRAW) - System.out.printf("redrawTextPage(): Area to clear: (%2f,%2f)–(%2f,%2f)%n", px0, py0, px1, py1); - if (background != null && background != Color.TRANSPARENT) { - context.setFill(background); - context.fillRect(px0, py0, px1 - px0, py1 - py0); - } else { - context.clearRect(px0, py0, px1 - px0, py1 - py0); - } - for (int tmpY = y0; tmpY <= y1; tmpY++) { - Char[] line = lineBuffer.get(tmpY - 1); - for (int tmpX = x0; tmpX <= x1; tmpX++) { - Char c = line[tmpX - 1]; - if (c != null) { - context.save(); - context.setFill(c.fill); - context.setStroke(c.stroke); - Paint bg = c.bg == background ? null : c.bg; - font.drawTextNoClearAt(context, (tmpX - 1) * getCharWidth(), tmpY * getCharHeight(), c.s, - textMode.getCharWidth() / textMode.getCharBoxSize(), c.mode/* m */, bg); - context.restore(); - // n++; - - } - } - } - /* - * long t = System.currentTimeMillis() - t0; if (m >= 0) - * System.out.printf(" %5d %5d %7.4f %4d %5b %5b %5b%n", t, n, ((double) t) / - * n, m, (m & 3) != 0, (m & 1) != 0, (m & 4) != 0); } System.out.println(); - */ - } - - public void resetAttrs() { - this.fill = DEFAULT_FILL; - this.stroke = DEFAULT_STROKE; - this.background = DEFAULT_BACKGROUND; - this.videoAttrs = 0; - this.csiSeq = null; - this.csiMode = 0; - } - - public void resetFull() { - resetAttrs(); - beginningOfPage(); - this.autoscroll = true; - this.textMode = DEFAULT_MODE; - redrawTextPage(); - } - - public void restoreCursor() { - x = savedX; - y = savedY; - } - - public void saveCursor() { - savedX = x; - savedY = y; - } - - void scroll(int i) { - while (i < 0) { - scrollDown(); - i++; - } - while (i > 0) { - scrollUp(); - i--; - } - } - - public void scrollDown() { - Char[] remove = lineBuffer.remove(lineBuffer.size() - 1); - Arrays.fill(remove, null); - lineBuffer.add(0, remove); - dirty(1, 1); - dirty(getLineWidth(), getPageHeight()); - if (!useBuffer) - redrawDirty(); - } - - public void scrollUp() { - Char[] remove = lineBuffer.remove(0); - Arrays.fill(remove, null); - lineBuffer.add(remove); - dirty(1, 1); - dirty(getLineWidth(), getPageHeight()); - if (!useBuffer) - redrawDirty(); - } - - public boolean setAutoScroll(boolean autoScroll) { - boolean old = autoscroll; - autoscroll = autoScroll; - return old; - } - - public void setBackground(int x, int y, Paint bg) { - Char c = null; - if (x > 0 && x <= TextMode.LINE_WIDTH_MAX && y > 0 && y <= TextMode.PAGE_HEIGHT_MAX) { - c = lineBuffer.get(y - 1)[x - 1]; - } - if (c != null) { - c.bg = bg; - drawChar(x, y, c); - } - } - - public void setBackground(Paint bgColor) { - this.background = bgColor != null ? bgColor : DEFAULT_BACKGROUND; - } - - public void setBold(boolean enabled) { - if (enabled) - videoAttrs |= TextFont.ATTR_BRIGHT; - else - videoAttrs &= ~TextFont.ATTR_BRIGHT; - } - - public Char setChar(int x, int y, String s) { - if (x > 0 && x <= TextMode.LINE_WIDTH_MAX && y > 0 && y <= TextMode.PAGE_HEIGHT_MAX) { - Char c = new Char(s, fill, stroke, background, videoAttrs); - lineBuffer.get(y - 1)[x - 1] = c; - return c; - } - return null; - } - - public void setColor(int x, int y, Color fill) { - Char c = null; - if (x > 0 && x <= TextMode.LINE_WIDTH_MAX && y > 0 && y <= TextMode.PAGE_HEIGHT_MAX) { - c = lineBuffer.get(y - 1)[x - 1]; - } - if (c != null) { - c.fill = fill; - drawChar(x, y, c); - } - } - - public void setFill(Color fill) { - this.fill = fill != null ? fill : DEFAULT_FILL; - } - - public void setFont(TextFont font) { - this.font = font; - } - - public void setInk(Color ink) { - fill = ink != null ? ink : DEFAULT_FILL; - stroke = ink != null ? ink : DEFAULT_STROKE; - } - - public void setItalics(boolean enabled) { - if (enabled) - videoAttrs |= TextFont.ATTR_ITALIC; - else - videoAttrs &= ~TextFont.ATTR_ITALIC; - } - - /** - */ - public void setLeftMargin() { - this.leftMargin = x; - } - - /** - * @param leftMargin - * the leftMargin to set - */ - public void setLeftMargin(int leftMargin) { - this.leftMargin = constrainX(leftMargin); - } - - public void setReverseVideo(boolean enabled) { - if (enabled) - videoAttrs |= TextFont.ATTR_INVERSE; - else - videoAttrs &= ~TextFont.ATTR_INVERSE; - } - - public void setStroke(Color stroke) { - this.stroke = stroke != null ? stroke : DEFAULT_STROKE; - } - - public void setTextMode(TextMode mode) { - setTextMode(mode, false); - } - - public void setTextMode(TextMode mode, boolean adjustDisplayAspect) { - if (mode == null) - throw new IllegalArgumentException(); - textMode = mode; - if (adjustDisplayAspect && screen != null) - screen.setAspect(textMode.getAspect()); - dirty(1, 1); - dirty(getLineWidth(), getPageHeight()); - if (!useBuffer) - redrawDirty(); - } - - public void setTopMargin() { - this.topMargin = y; - } - - /** - * @param topMargin - * the topMargin to set - */ - public void setTopMargin(int topMargin) { - this.topMargin = constrainY(topMargin); - } - - public void setVideoAttrDisabled(int attr) { - videoAttrs &= ~attr; - } - - public void setVideoAttrEnabled(int attr) { - videoAttrs |= attr; - } - - public void setVideoAttrs(int attr) { - videoAttrs = attr; - } - - public void unplot(int x, int y) { - plot(x, y, (a, b) -> a & ~b); - } - - @Override - public double getWidth() { - return width; - } - - @Override - public double getHeight() { - return height; - } - - private boolean isDirty() { - return dirtyX0 <= dirtyX1 || dirtyY0 <= dirtyY1; - } - - /** - * Expand the dirty region (area that should be redrawn) to include the given - * position - * - * @param x - * @param y - */ - private void dirty(int x, int y) { - dirtyX0 = Math.max(Math.min(x, dirtyX0), 1); - dirtyX1 = Math.min(Math.max(x, dirtyX1), getLineWidth()); - dirtyY0 = Math.max(Math.min(y, dirtyY0), 1); - dirtyY1 = Math.min(Math.max(y, dirtyY1), getPageHeight()); - } - - /** - * Redraw the part of the page that has changed since last redraw. - */ - public void redrawDirty() { - if (isDirty()) { - if (DEBUG_REDRAW) - System.out.printf("redrawDirty(): Dirty region is (%d,%d)–(%d,%d)%n", dirtyX0, dirtyY0, dirtyX1, - dirtyY1); - redrawTextPage(dirtyX0, dirtyY0, dirtyX1, dirtyY1); - clean(); - } - } - - /** - * Mark the entire page as clean - */ - private void clean() { - dirtyX0 = Integer.MAX_VALUE; - dirtyX1 = Integer.MIN_VALUE; - dirtyY0 = Integer.MAX_VALUE; - dirtyY1 = Integer.MIN_VALUE; - } - - /** - * With buffered printing, nothing is actually drawn until - * {@link #redrawDirty()} or {@link #redrawTextPage()} is called. - * - * @param buffering - * Whether to use buffering - */ - public void setBuffering(boolean buffering) { - useBuffer = buffering; - } - - /** - * @return True if buffering is enabled - * @see #setBuffering(boolean) - */ - public boolean getBuffering() { - return useBuffer; - } + + private static class Char { + + public int mode; + public String s; + public Color fill; + public Color stroke; + public Paint bg; + + public Char(String s, Color fill, Color stroke, Paint bg, int mode) { + this.s = s; + this.fill = fill; + this.stroke = stroke; + this.bg = bg; + this.mode = mode; + } + + } + + public static final TextFont FONT_MONOSPACED = new TextFont("Monospaced", 27.00, TextMode.CHAR_BOX_SIZE, 3.4000, + -6.7000, 1.5000, 1.0000, true); + public static final TextFont FONT_LMMONO = new TextFont("lmmono10-regular.otf", 30.00, TextMode.CHAR_BOX_SIZE, + 4.0000, -8.5000, 1.5000, 1.0000, true); + public static final TextFont FONT_ZXSPECTRUM7 = new TextFont("ZXSpectrum-7.otf", 22.00, TextMode.CHAR_BOX_SIZE, + 3.1000, -3.8000, 1.0000, 1.0000, true); + + /** + * TTF file can be found here: http://users.teilar.gr/~g1951d/ in this ZIP file: + * http://users.teilar.gr/~g1951d/Symbola.zip + *

    + * (Put the extracted Symbola.ttf in src/inf101/v18/gfx/fonts/) + */ + public static final TextFont FONT_SYMBOLA = new TextFont("Symbola.ttf", 26.70, TextMode.CHAR_BOX_SIZE, -0.4000, + -7.6000, 1.35000, 1.0000, true); + + /** + * TTF file can be found here: + * http://www.kreativekorp.com/software/fonts/c64.shtml + */ + public static final TextFont FONT_GIANA = new TextFont("Giana.ttf", 25.00, TextMode.CHAR_BOX_SIZE, 4.6000, -5.0000, + 1.0000, 1.0000, true); + + /** + * TTF file can be found here: + * http://www.kreativekorp.com/software/fonts/c64.shtml + */ + public static final TextFont FONT_C64 = new TextFont("PetMe64.ttf", 31.50, TextMode.CHAR_BOX_SIZE, 0.0000, -4.000, + 1.0000, 1.0000, true); + private static final Paint DEFAULT_BACKGROUND = Color.TRANSPARENT; + private static final TextMode DEFAULT_MODE = TextMode.MODE_40X22; + private static final boolean DEBUG_REDRAW = false; + + public static String center(String s, int width) { + for (; s.length() < width; s = " " + s + " ") + ; + return s; + } + + public static String repeat(String s, int width) { + String r = s; + for (; r.length() < width; r += s) + ; + return r; + } + + Color DEFAULT_FILL = Color.WHITE; + + Color DEFAULT_STROKE = Color.TRANSPARENT; + + private TextMode textMode; + private Color fill; + private Color stroke; + private Paint background; + private Screen screen; + private List lineBuffer = new ArrayList<>(); + private boolean autoscroll = true; + private final Canvas textPage; + private int x = 1, y = 1, savedX = 1, savedY = 1; + // private int pageWidth = LINE_WIDTHS[resMode], pageHeight = + // PAGE_HEIGHTS[resMode]; + private int leftMargin = 1, topMargin = 1; + private TextFont font = FONT_SYMBOLA; + private int videoAttrs = 0; + + private String csiSeq = null; + + private boolean csiEnabled = true; + + private int csiMode = 0; + + private final double width; + private final double height; + + private int dirtyX0 = Integer.MAX_VALUE; + private int dirtyX1 = Integer.MIN_VALUE; + private int dirtyY0 = Integer.MAX_VALUE; + private int dirtyY1 = Integer.MIN_VALUE; + private boolean useBuffer = true; + + public Printer(double width, double height) { + this.screen = null; + this.textPage = null; + this.width = width; + this.height = height; + for (int i = 0; i < TextMode.PAGE_HEIGHT_MAX; i++) { + lineBuffer.add(new Char[TextMode.LINE_WIDTH_MAX]); + } + resetFull(); + } + + public Printer(Screen screen, Canvas page) { + this.screen = screen; + this.textPage = page; + this.width = page.getWidth(); + this.height = page.getHeight(); + for (int i = 0; i < TextMode.PAGE_HEIGHT_MAX; i++) { + lineBuffer.add(new Char[TextMode.LINE_WIDTH_MAX]); + } + resetFull(); + } + + public void addToCharBuffer(String string) { + string.codePoints().mapToObj((int i) -> String.valueOf(Character.toChars(i))).forEach((String s) -> { + if (csiMode != 0) { + s = addToCsiBuffer(s); + } + switch (s) { + case "\r": + moveTo(leftMargin, y); + break; + case "\n": + moveTo(leftMargin, y + 1); + break; + case "\f": + moveTo(leftMargin, topMargin); + for (Char[] line : lineBuffer) + Arrays.fill(line, null); + if (textPage != null) { + GraphicsContext context = textPage.getGraphicsContext2D(); + if (background != null && background != Color.TRANSPARENT) { + context.setFill(background); + context.fillRect(0.0, 0.0, textPage.getWidth(), textPage.getHeight()); + } else { + context.clearRect(0.0, 0.0, textPage.getWidth(), textPage.getHeight()); + } + } + break; + case "\b": + moveHoriz(-1); + break; + case "\t": + moveTo((x + 8) % 8, y); + break; + case "\u001b": + if (csiEnabled) { + csiSeq = s; + csiMode = 1; + } + break; + default: + if (s.length() > 0 && s.codePointAt(0) >= 0x20) { + drawChar(x, y, setChar(x, y, s)); + moveHoriz(1); + } + break; + } + }); + } + + private String addToCsiBuffer(String s) { + if (csiMode == 1) { + switch (s) { + case "[": + csiMode = 2; + csiSeq += s; + break; + case "c": + csiMode = 0; + resetFull(); + break; + default: + csiReset(); + return s; + } + } else if (csiMode == 2) { + int c = s.codePointAt(0); + if (c >= 0x30 && c <= 0x3f) { + csiSeq += s; + } else if (c >= 0x20 && c <= 0x2f) { + csiMode = 3; + csiSeq += s; + } else if (c >= 0x40 && c <= 0x7e) { + csiSeq += s; + csiFinish(); + } else { + csiReset(); + return s; + } + + } else if (csiMode == 3) { + int c = s.codePointAt(0); + if (c >= 0x20 && c <= 0x2f) { + csiSeq += s; + } else if (c >= 0x40 && c <= 0x7e) { + csiSeq += s; + csiFinish(); + } else { + csiReset(); + return s; + } + } + return ""; + } + + public void beginningOfLine() { + x = leftMargin; + } + + public void beginningOfPage() { + x = leftMargin; + y = topMargin; + } + + @Override + public void clear() { + print("\f"); + } + + public void clearAt(int x, int y) { + printAt(x, y, " "); + } + + public void clearLine(int y) { + y = constrainY(y); + if (y > 0 && y <= TextMode.PAGE_HEIGHT_MAX) { + Arrays.fill(lineBuffer.get(y - 1), null); + dirty(1, y); + dirty(getLineWidth(), y); + if (!useBuffer) { + redrawDirty(); + } + } + } + + public void clearRegion(int x, int y, int width, int height) { + if (x > getLineWidth() || y > getPageHeight()) { + return; + } + int x2 = Math.min(x + width - 1, getLineWidth()); + int y2 = Math.min(y + height - 1, getPageHeight()); + if (x2 < 1 || y2 < 1) { + return; + } + int x1 = Math.max(1, x); + int y1 = Math.max(1, y); + // Char fillWith = new Char("*", Color.BLACK, Color.GREEN, Color.TRANSPARENT, + // 0); + for (int i = y1; i <= y2; i++) { + Arrays.fill(lineBuffer.get(i - 1), x1 - 1, x2, null); + } + dirty(x1, y1); + dirty(x2, y2); + if (!useBuffer) { + redrawDirty(); + } + } + + private int constrainX(int x) { + return x; // Math.min(LINE_WIDTH_HIRES, Math.max(1, x)); + } + + public int constrainY(int y) { + return y; // Math.min(pageHeight, Math.max(1, y)); + } + + public int constrainYOrScroll(int y) { + if (autoscroll) { + if (y < 1) { + scroll(y - 1); + return 1; + } else if (y > getPageHeight()) { + scroll(y - getPageHeight()); + return getPageHeight(); + } + } + + return y;// Math.min(pageHeight, Math.max(1, y)); + } + + private void csiFinish() { + ControlSequences.applyCsi(this, csiSeq); + csiReset(); + } + + private void csiReset() { + csiMode = 0; + csiSeq = null; + } + + public void cycleMode(boolean adjustDisplayAspect) { + textMode = textMode.nextMode(); + if (adjustDisplayAspect && screen != null) { + screen.setAspect(textMode.getAspect()); + } + dirty(1, 1); + dirty(getLineWidth(), getPageHeight()); + if (!useBuffer) { + redrawDirty(); + } + } + + private void drawChar(int x, int y, Char c) { + if (useBuffer) { + dirty(x, y); + } else if (c != null && textPage != null) { + GraphicsContext context = textPage.getGraphicsContext2D(); + + context.setFill(c.fill); + context.setStroke(c.stroke); + font.drawTextAt(context, (x - 1) * getCharWidth(), y * getCharHeight(), c.s, + textMode.getCharWidth() / textMode.getCharBoxSize(), c.mode, c.bg); + } + } + + public void drawCharCells() { + if (screen != null) { + GraphicsContext context = screen.getBackgroundContext(); + screen.clearBackground(); + double w = getCharWidth(); + double h = getCharHeight(); + context.save(); + context.setGlobalBlendMode(BlendMode.EXCLUSION); + context.setFill(Color.WHITE.deriveColor(0.0, 1.0, 1.0, 0.1)); + for (int x = 0; x < getLineWidth(); x++) { + for (int y = 0; y < getPageHeight(); y++) { + if ((x + y) % 2 == 0) { + context.fillRect(x * w, y * h, w, h); + } + } + } + context.restore(); + } + } + + public Color getBackground(int x, int y) { + Char c = null; + if (x > 0 && x <= TextMode.LINE_WIDTH_MAX && y > 0 && y <= TextMode.PAGE_HEIGHT_MAX) { + c = lineBuffer.get(y - 1)[x - 1]; + } + Color bg = Color.TRANSPARENT; + if (c != null && c.bg instanceof Color) { + bg = (Color) c.bg; + } else if (background instanceof Color) { + bg = (Color) background; + } + return bg; + } + + public boolean getBold() { + return (videoAttrs & TextFont.ATTR_BRIGHT) != 0; + } + + public String getChar(int x, int y) { + Char c = null; + if (x > 0 && x <= TextMode.LINE_WIDTH_MAX && y > 0 && y <= TextMode.PAGE_HEIGHT_MAX) { + c = lineBuffer.get(y - 1)[x - 1]; + } + if (c != null) { + return c.s; + } else { + return " "; + } + } + + public double getCharHeight() { + return textMode.getCharHeight(); + } + + public double getCharWidth() { + return textMode.getCharWidth(); + } + + public Color getColor(int x, int y) { + Char c = null; + if (x > 0 && x <= TextMode.LINE_WIDTH_MAX && y > 0 && y <= TextMode.PAGE_HEIGHT_MAX) { + c = lineBuffer.get(y - 1)[x - 1]; + } + if (c != null) { + return c.fill; + } else { + return fill; + } + } + + public TextFont getFont() { + return font; + } + + public boolean getItalics() { + return (videoAttrs & TextFont.ATTR_ITALIC) != 0; + } + + /** + * @return the leftMargin + */ + public int getLeftMargin() { + return leftMargin; + } + + public int getLineWidth() { + return textMode.getLineWidth(); + } + + public int getPageHeight() { + return textMode.getPageHeight(); + } + + public boolean getReverseVideo() { + return (videoAttrs & TextFont.ATTR_INVERSE) != 0; + } + + public TextMode getTextMode() { + return textMode; + } + + /** + * @return the topMargin + */ + public int getTopMargin() { + return topMargin; + } + + public int getVideoMode() { + return videoAttrs; + } + + public int getX() { + return x; + } + + public int getY() { + return y; + } + + public boolean isFilled(int x, int y) { + return !getChar(x, y).equals(" "); + } + + @Override + public void layerToBack() { + if (screen != null) { + screen.moveToBack(this); + } + } + + @Override + public void layerToFront() { + if (screen != null) { + screen.moveToFront(this); + } + } + + public void move(int deltaX, int deltaY) { + x = constrainX(x + deltaX); + y = constrainYOrScroll(y + deltaY); + } + + public void moveHoriz(int dist) { + x = constrainX(x + dist); + } + + public void moveTo(int newX, int newY) { + x = constrainX(newX); + y = constrainYOrScroll(newY); + } + + public void moveVert(int dist) { + y = constrainYOrScroll(y + dist); + } + + public void plot(int x, int y) { + plot(x, y, (a, b) -> a | b); + } + + public void plot(int x, int y, BiFunction op) { + int textX = (x) / 2 + 1; + int textY = (y) / 2 + 1; + int bitPos = (x + 1) % 2 + ((y + 1) % 2) * 2; + String blockChar = BlocksAndBoxes.unicodeBlocks[1 << bitPos]; + // System.out.println(blockChar + ", " + bitPos + ", ("+ (x) + ", " + (y) + ")"+ + // ", (" + (textX) + ", " + (textY) + ")"); + String s = BlocksAndBoxes.blockComposeOrOverwrite(getChar(textX, textY), blockChar, op); + // System.out.println("Merge '" + getChar(textX, textY) + "' + '" + blockChar + + // "' = '" + s + "'"); + printAt(textX, textY, s); + } + + public void print(String s) { + addToCharBuffer(s); + } + + public void print(String s, Color paint) { + Color tmp = fill; + fill = paint; + addToCharBuffer(s); + fill = tmp; + } + + public void printAt(int atX, int atY, String s) { + moveTo(atX, atY); + print(s); + } + + public void printAt(int atX, int atY, String s, Color ink) { + moveTo(atX, atY); + print(s, ink); + } + + public void println() { + print("\n"); + } + + public void println(String s) { + print(s); + print("\n"); + } + + public void redrawTextPage() { + redrawTextPage(1, 1, getLineWidth(), getPageHeight()); + clean(); + } + + private void redrawTextPage(int x0, int y0, int x1, int y1) { + /* + * System.out.printf("redrawTextPage benchmark"); + * System.out.printf(" %5s %5s %7s %4s %5s %5s %5s%n", "ms", "chars", + * "ms/char", "mode", "indir", "inv", "fake"); for (int m = -1; m < 8; m++) { + * long t0 = System.currentTimeMillis(); int n = 0; + */ + if (textPage == null) { + return; + } + GraphicsContext context = textPage.getGraphicsContext2D(); + + double px0 = (x0 - 1) * getCharWidth(), py0 = (y0 - 1) * getCharHeight(); + double px1 = x1 * getCharWidth(), py1 = y1 * getCharHeight(); + if (DEBUG_REDRAW) { + System.out.printf("redrawTextPage(): Area to clear: (%2f,%2f)–(%2f,%2f)%n", px0, py0, px1, py1); + } + if (background != null && background != Color.TRANSPARENT) { + context.setFill(background); + context.fillRect(px0, py0, px1 - px0, py1 - py0); + } else { + context.clearRect(px0, py0, px1 - px0, py1 - py0); + } + for (int tmpY = y0; tmpY <= y1; tmpY++) { + Char[] line = lineBuffer.get(tmpY - 1); + for (int tmpX = x0; tmpX <= x1; tmpX++) { + Char c = line[tmpX - 1]; + if (c != null) { + context.save(); + context.setFill(c.fill); + context.setStroke(c.stroke); + Paint bg = c.bg == background ? null : c.bg; + font.drawTextNoClearAt(context, (tmpX - 1) * getCharWidth(), tmpY * getCharHeight(), c.s, + textMode.getCharWidth() / textMode.getCharBoxSize(), c.mode/* m */, bg); + context.restore(); + // n++; + + } + } + } + /* + * long t = System.currentTimeMillis() - t0; if (m >= 0) + * System.out.printf(" %5d %5d %7.4f %4d %5b %5b %5b%n", t, n, ((double) t) / + * n, m, (m & 3) != 0, (m & 1) != 0, (m & 4) != 0); } System.out.println(); + */ + } + + public void resetAttrs() { + this.fill = DEFAULT_FILL; + this.stroke = DEFAULT_STROKE; + this.background = DEFAULT_BACKGROUND; + this.videoAttrs = 0; + this.csiSeq = null; + this.csiMode = 0; + } + + public void resetFull() { + resetAttrs(); + beginningOfPage(); + this.autoscroll = true; + this.textMode = DEFAULT_MODE; + redrawTextPage(); + } + + public void restoreCursor() { + x = savedX; + y = savedY; + } + + public void saveCursor() { + savedX = x; + savedY = y; + } + + void scroll(int i) { + while (i < 0) { + scrollDown(); + i++; + } + while (i > 0) { + scrollUp(); + i--; + } + } + + public void scrollDown() { + Char[] remove = lineBuffer.remove(lineBuffer.size() - 1); + Arrays.fill(remove, null); + lineBuffer.add(0, remove); + dirty(1, 1); + dirty(getLineWidth(), getPageHeight()); + if (!useBuffer) { + redrawDirty(); + } + } + + public void scrollUp() { + Char[] remove = lineBuffer.remove(0); + Arrays.fill(remove, null); + lineBuffer.add(remove); + dirty(1, 1); + dirty(getLineWidth(), getPageHeight()); + if (!useBuffer) { + redrawDirty(); + } + } + + public boolean setAutoScroll(boolean autoScroll) { + boolean old = autoscroll; + autoscroll = autoScroll; + return old; + } + + public void setBackground(int x, int y, Paint bg) { + Char c = null; + if (x > 0 && x <= TextMode.LINE_WIDTH_MAX && y > 0 && y <= TextMode.PAGE_HEIGHT_MAX) { + c = lineBuffer.get(y - 1)[x - 1]; + } + if (c != null) { + c.bg = bg; + drawChar(x, y, c); + } + } + + public void setBackground(Paint bgColor) { + this.background = bgColor != null ? bgColor : DEFAULT_BACKGROUND; + } + + public void setBold(boolean enabled) { + if (enabled) { + videoAttrs |= TextFont.ATTR_BRIGHT; + } else { + videoAttrs &= ~TextFont.ATTR_BRIGHT; + } + } + + public Char setChar(int x, int y, String s) { + if (x > 0 && x <= TextMode.LINE_WIDTH_MAX && y > 0 && y <= TextMode.PAGE_HEIGHT_MAX) { + Char c = new Char(s, fill, stroke, background, videoAttrs); + lineBuffer.get(y - 1)[x - 1] = c; + return c; + } + return null; + } + + public void setColor(int x, int y, Color fill) { + Char c = null; + if (x > 0 && x <= TextMode.LINE_WIDTH_MAX && y > 0 && y <= TextMode.PAGE_HEIGHT_MAX) { + c = lineBuffer.get(y - 1)[x - 1]; + } + if (c != null) { + c.fill = fill; + drawChar(x, y, c); + } + } + + public void setFill(Color fill) { + this.fill = fill != null ? fill : DEFAULT_FILL; + } + + public void setFont(TextFont font) { + this.font = font; + } + + public void setInk(Color ink) { + fill = ink != null ? ink : DEFAULT_FILL; + stroke = ink != null ? ink : DEFAULT_STROKE; + } + + public void setItalics(boolean enabled) { + if (enabled) { + videoAttrs |= TextFont.ATTR_ITALIC; + } else { + videoAttrs &= ~TextFont.ATTR_ITALIC; + } + } + + /** + * + */ + public void setLeftMargin() { + this.leftMargin = x; + } + + /** + * @param leftMargin the leftMargin to set + */ + public void setLeftMargin(int leftMargin) { + this.leftMargin = constrainX(leftMargin); + } + + public void setReverseVideo(boolean enabled) { + if (enabled) { + videoAttrs |= TextFont.ATTR_INVERSE; + } else { + videoAttrs &= ~TextFont.ATTR_INVERSE; + } + } + + public void setStroke(Color stroke) { + this.stroke = stroke != null ? stroke : DEFAULT_STROKE; + } + + public void setTextMode(TextMode mode) { + setTextMode(mode, false); + } + + public void setTextMode(TextMode mode, boolean adjustDisplayAspect) { + if (mode == null) { + throw new IllegalArgumentException(); + } + textMode = mode; + if (adjustDisplayAspect && screen != null) { + screen.setAspect(textMode.getAspect()); + } + dirty(1, 1); + dirty(getLineWidth(), getPageHeight()); + if (!useBuffer) { + redrawDirty(); + } + } + + public void setTopMargin() { + this.topMargin = y; + } + + /** + * @param topMargin the topMargin to set + */ + public void setTopMargin(int topMargin) { + this.topMargin = constrainY(topMargin); + } + + public void setVideoAttrDisabled(int attr) { + videoAttrs &= ~attr; + } + + public void setVideoAttrEnabled(int attr) { + videoAttrs |= attr; + } + + public void setVideoAttrs(int attr) { + videoAttrs = attr; + } + + public void unplot(int x, int y) { + plot(x, y, (a, b) -> a & ~b); + } + + @Override + public double getWidth() { + return width; + } + + @Override + public double getHeight() { + return height; + } + + private boolean isDirty() { + return dirtyX0 <= dirtyX1 || dirtyY0 <= dirtyY1; + } + + /** + * Expand the dirty region (area that should be redrawn) to include the given + * position + * + * @param x + * @param y + */ + private void dirty(int x, int y) { + dirtyX0 = Math.max(Math.min(x, dirtyX0), 1); + dirtyX1 = Math.min(Math.max(x, dirtyX1), getLineWidth()); + dirtyY0 = Math.max(Math.min(y, dirtyY0), 1); + dirtyY1 = Math.min(Math.max(y, dirtyY1), getPageHeight()); + } + + /** + * Redraw the part of the page that has changed since last redraw. + */ + public void redrawDirty() { + if (isDirty()) { + if (DEBUG_REDRAW) { + System.out.printf("redrawDirty(): Dirty region is (%d,%d)–(%d,%d)%n", dirtyX0, dirtyY0, dirtyX1, + dirtyY1); + } + redrawTextPage(dirtyX0, dirtyY0, dirtyX1, dirtyY1); + clean(); + } + } + + /** + * Mark the entire page as clean + */ + private void clean() { + dirtyX0 = Integer.MAX_VALUE; + dirtyX1 = Integer.MIN_VALUE; + dirtyY0 = Integer.MAX_VALUE; + dirtyY1 = Integer.MIN_VALUE; + } + + /** + * With buffered printing, nothing is actually drawn until + * {@link #redrawDirty()} or {@link #redrawTextPage()} is called. + * + * @param buffering Whether to use buffering + */ + public void setBuffering(boolean buffering) { + useBuffer = buffering; + } + + /** + * @return True if buffering is enabled + * @see #setBuffering(boolean) + */ + public boolean getBuffering() { + return useBuffer; + } + } diff --git a/src/main/java/inf101/v18/gfx/textmode/TextFont.java b/src/main/java/inf101/v18/gfx/textmode/TextFont.java index bdfa144..bfc8277 100644 --- a/src/main/java/inf101/v18/gfx/textmode/TextFont.java +++ b/src/main/java/inf101/v18/gfx/textmode/TextFont.java @@ -1,11 +1,5 @@ package inf101.v18.gfx.textmode; -import java.io.FileNotFoundException; -import java.io.IOException; -import java.io.InputStream; -import java.util.HashMap; -import java.util.Map; - import javafx.geometry.Point2D; import javafx.scene.SnapshotParameters; import javafx.scene.canvas.Canvas; @@ -18,980 +12,944 @@ import javafx.scene.paint.Color; import javafx.scene.paint.Paint; import javafx.scene.shape.StrokeLineCap; import javafx.scene.text.Font; -import javafx.scene.text.Text; import javafx.scene.transform.Affine; import javafx.scene.transform.Transform; +import java.io.FileNotFoundException; +import java.io.IOException; +import java.io.InputStream; +import java.util.HashMap; +import java.util.Map; + /** * TextFont – for grid-based text / character graphics + * *

    * TextFonts are used for drawing text and character graphics. Each character is * assumed to be uniform in size, fitting within a square with sides of length * {@link #getSquareSize()}. The given font file should contain a monospaced * font of a type suitable for JavaFX, such as OpenType or TrueType. - * + *

    + * *

    * Additional horizontal and vertical positioning and scaling can be used to * make the font fit with the square-shaped concept. - * + *

    + * *

    * See {@link #setGraphicsContext(GraphicsContext)} for setting up the graphics * context for writing with the font, or * {@link #setGraphicsContext(GraphicsContext, double)} for extra on-the-fly * horizontal scaling, e.g. to make half-width letters ("hires" mode). - * + *

    */ public class TextFont { - public static final int ATTR_INVERSE = 0x01; - public static final int ATTR_ITALIC = 0x02; - public static final int ATTR_BOLD = 0x04; - public static final int ATTR_OUTLINE = 0x08; - public static final int ATTR_UNDERLINE = 0x10; - public static final int ATTR_OVERLINE = 0x20; - public static final int ATTR_LINE_THROUGH = 0x40; - public static final int ATTR_OVERSTRIKE = 0x80; - public static final int ATTR_CLIP = 0x80; - public static final int ATTR_NO_FAKE_CHARS = 0x100; - public static final int ATTR_BLINK = 0x200; // NOT IMPLEMENTED - public static final int ATTR_FAINT = 0x400; // NOT IMPLEMENTED - public static final int ATTR_BRIGHT = 0x800; + public static final int ATTR_INVERSE = 0x01; + public static final int ATTR_ITALIC = 0x02; + public static final int ATTR_BOLD = 0x04; + public static final int ATTR_OUTLINE = 0x08; + public static final int ATTR_UNDERLINE = 0x10; + public static final int ATTR_OVERLINE = 0x20; + public static final int ATTR_LINE_THROUGH = 0x40; + public static final int ATTR_OVERSTRIKE = 0x80; + public static final int ATTR_CLIP = 0x80; + public static final int ATTR_NO_FAKE_CHARS = 0x100; + public static final int ATTR_BLINK = 0x200; // NOT IMPLEMENTED + public static final int ATTR_FAINT = 0x400; // NOT IMPLEMENTED + public static final int ATTR_BRIGHT = 0x800; - private static final String[] searchPath = { "", "../", "fonts/" }; - private static final Map loadedFonts = new HashMap<>(); - private static final double thin = 2.0, thick = 4.0; - private static final String[] boxDrawingShapes = { // lines - "--..", "**..", "..--", "..**", - // dashed lines - "3--..", "3**..", "3..--", "3..**", "4--..", "4**..", "4..--", "4..**", - // corners - ".-.-", ".*.-", ".-.*", ".*.*", "-..-", "*..-", "-..*", "*..*", ".--.", ".*-.", ".-*.", ".**.", "-.-.", - "*.-.", "-.*.", "*.*.", - // |- - ".---", ".*--", ".-*- ", ".--* ", ".-**", ".**-", ".*-*", ".***", "-.--", "*.--", "-.*-", "-.-*", "-.**", - "*.*-", "*.-*", "*.**", - // T - "--.-", "*-.-", "-*.-", "**.-", "--.*", "*-.*", "-*.*", "**.*", "---.", "*--. ", "-*-.", "**-.", "--*.", - "*-*.", "-**.", "***.", - // + - "----", "*---", "-*--", "**--", "--*-", "---*", "--**", "*-*-", "-**-", "*--*", "-*-*", "***-", "**-*", - "*-**", "-***", "****", - // dashes - "2--..", "2**..", "2..--", "2..**", - // double lines - "==..", "..==", ".=.-", ".-.=", ".N.N", "=..-", "-..=", "M..M", ".=-.", ".-=.", ".MM.", "=.-.", "-.=.", - "N.N.", ".=--", ".s==", ".zMN", "=.--", "s.==", "z.NM", "==.s", "--.=", "MN.z", "==s.", "--=.", "NMz.", - "==--", "--==", "zzzz", - // round corners - "0.-.-", "0-..-", "0-.-.", "0.--.", - // diagonals - "////", "\\\\\\\\", "//\\\\\\", - // short lines - "-...", "..-.", ".-..", "...-", "*...", "..*.", ".*..", "...*", - // thin/thick lines - "-*..", "..-*", "*-..", "..*-" }; + private static final String[] searchPath = {"", "../", "fonts/"}; + private static final Map loadedFonts = new HashMap<>(); + private static final double thin = 2.0, thick = 4.0; + private static final String[] boxDrawingShapes = { // lines + "--..", "**..", "..--", "..**", + // dashed lines + "3--..", "3**..", "3..--", "3..**", "4--..", "4**..", "4..--", "4..**", + // corners + ".-.-", ".*.-", ".-.*", ".*.*", "-..-", "*..-", "-..*", "*..*", ".--.", ".*-.", ".-*.", ".**.", "-.-.", + "*.-.", "-.*.", "*.*.", + // |- + ".---", ".*--", ".-*- ", ".--* ", ".-**", ".**-", ".*-*", ".***", "-.--", "*.--", "-.*-", "-.-*", "-.**", + "*.*-", "*.-*", "*.**", + // T + "--.-", "*-.-", "-*.-", "**.-", "--.*", "*-.*", "-*.*", "**.*", "---.", "*--. ", "-*-.", "**-.", "--*.", + "*-*.", "-**.", "***.", + // + + "----", "*---", "-*--", "**--", "--*-", "---*", "--**", "*-*-", "-**-", "*--*", "-*-*", "***-", "**-*", + "*-**", "-***", "****", + // dashes + "2--..", "2**..", "2..--", "2..**", + // double lines + "==..", "..==", ".=.-", ".-.=", ".N.N", "=..-", "-..=", "M..M", ".=-.", ".-=.", ".MM.", "=.-.", "-.=.", + "N.N.", ".=--", ".s==", ".zMN", "=.--", "s.==", "z.NM", "==.s", "--.=", "MN.z", "==s.", "--=.", "NMz.", + "==--", "--==", "zzzz", + // round corners + "0.-.-", "0-..-", "0-.-.", "0.--.", + // diagonals + "////", "\\\\\\\\", "//\\\\\\", + // short lines + "-...", "..-.", ".-..", "...-", "*...", "..*.", ".*..", "...*", + // thin/thick lines + "-*..", "..-*", "*-..", "..*-"}; - public static void drawMasked(GraphicsContext ctx, Image src, Image mask, double x, double y, boolean invert, - boolean blend) { - int w = (int) src.getWidth(); - int h = (int) src.getHeight(); + public static void drawMasked(GraphicsContext ctx, Image src, Image mask, double x, double y, boolean invert, + boolean blend) { + int w = (int) src.getWidth(); + int h = (int) src.getHeight(); - PixelReader pixelSrc = src.getPixelReader(); - PixelReader pixelMask = mask.getPixelReader(); - PixelWriter pixelWriter = ctx.getPixelWriter(); - Affine transform = ctx.getTransform(); - Point2D point = transform.transform(x, y); - int dx = (int) point.getX(), dy = (int) point.getY(); + PixelReader pixelSrc = src.getPixelReader(); + PixelReader pixelMask = mask.getPixelReader(); + PixelWriter pixelWriter = ctx.getPixelWriter(); + Affine transform = ctx.getTransform(); + Point2D point = transform.transform(x, y); + int dx = (int) point.getX(), dy = (int) point.getY(); - for (int px = 0; px < w; px++) { - for (int py = 0; py < h; py++) { - int a = pixelMask.getArgb(px, py) >>> 24; - int rgb = pixelSrc.getArgb(px, py); - if (invert) - a = ~a & 0xff; - if (blend) - a = ((rgb >>> 24) * a) >>> 8; - pixelWriter.setArgb(px + dx, py + dy, (a << 24) | rgb); + for (int px = 0; px < w; px++) { + for (int py = 0; py < h; py++) { + int a = pixelMask.getArgb(px, py) >>> 24; + int rgb = pixelSrc.getArgb(px, py); + if (invert) { + a = ~a & 0xff; + } + if (blend) { + a = ((rgb >>> 24) * a) >>> 8; + } + pixelWriter.setArgb(px + dx, py + dy, (a << 24) | rgb); - } - } - } + } + } + } - public static void fillInverse(GraphicsContext ctx, Image img, double x, double y) { - int w = (int) img.getWidth(); - int h = (int) img.getHeight(); - PixelReader pixelReader = img.getPixelReader(); - PixelWriter pixelWriter = ctx.getPixelWriter(); - Affine transform = ctx.getTransform(); - Point2D point = transform.transform(x, y); - int dx = (int) point.getX(), dy = (int) point.getY(); - Color c = ctx.getFill() instanceof Color ? (Color) ctx.getFill() : Color.BLACK; - int rgb = ((int) (c.getRed() * 255)) << 16 | ((int) (c.getGreen() * 255)) << 8 | ((int) (c.getBlue() * 255)); + public static void fillInverse(GraphicsContext ctx, Image img, double x, double y) { + int w = (int) img.getWidth(); + int h = (int) img.getHeight(); + PixelReader pixelReader = img.getPixelReader(); + PixelWriter pixelWriter = ctx.getPixelWriter(); + Affine transform = ctx.getTransform(); + Point2D point = transform.transform(x, y); + int dx = (int) point.getX(), dy = (int) point.getY(); + Color c = ctx.getFill() instanceof Color ? (Color) ctx.getFill() : Color.BLACK; + int rgb = ((int) (c.getRed() * 255)) << 16 | ((int) (c.getGreen() * 255)) << 8 | ((int) (c.getBlue() * 255)); - for (int px = 0; px < w; px++) { - for (int py = 0; py < h; py++) { - int a = (~pixelReader.getArgb(px, py) & 0xff000000); - // if(a != 0) - pixelWriter.setArgb(px + dx, py + dy, a | rgb); + for (int px = 0; px < w; px++) { + for (int py = 0; py < h; py++) { + int a = (~pixelReader.getArgb(px, py) & 0xff000000); + // if(a != 0) + pixelWriter.setArgb(px + dx, py + dy, a | rgb); - } - } - } + } + } + } - /** - * Load a font. - * - * Will first try to use the provided string as a file name, and load the font - * from a font file (should be in a format supported by JavaFX). If loading from - * file fails, we assume we have been given the name of a font which is passed - * directly to JavaFX for loading. A fallback font is used if this fails. - * - * When looking for files, his method will search for the file in a search path - * starting with the folder containing the {@link TextFont} class file (using - * Java's standard {@link Class#getResourceAsStream(String)} system). The search - * path also includes ".." (parent directory) and "../fonts". - * - * The loaded font will be cached, so that additional calls with the same file - * name will not cause the file to be loaded again. - * - * If the font cannot be loaded, a default font will be substituted. You may - * check which font you got using {@link Font#getName()} or - * {@link Font#getFamily()}. - * - * @param name - * Font name, or relative path to the file - * @param size - * Desired point size of the font - * @return A JavaFX font - */ - public static final Font findFont(String name, double size) { - return findFont(name, size, TextFont.class); - } + /** + * Load a font. + *

    + * Will first try to use the provided string as a file name, and load the font + * from a font file (should be in a format supported by JavaFX). If loading from + * file fails, we assume we have been given the name of a font which is passed + * directly to JavaFX for loading. A fallback font is used if this fails. + *

    + * When looking for files, his method will search for the file in a search path + * starting with the folder containing the {@link TextFont} class file (using + * Java's standard {@link Class#getResourceAsStream(String)} system). The search + * path also includes ".." (parent directory) and "../fonts". + *

    + * The loaded font will be cached, so that additional calls with the same file + * name will not cause the file to be loaded again. + *

    + * If the font cannot be loaded, a default font will be substituted. You may + * check which font you got using {@link Font#getName()} or + * {@link Font#getFamily()}. + * + * @param name Font name, or relative path to the file + * @param size Desired point size of the font + * @return A JavaFX font + */ + public static final Font findFont(String name, double size) { + return findFont(name, size, TextFont.class); + } - /** - * Load a font. - * - * Will first try to use the provided string as a file name, and load the font - * from a font file (should be in a format supported by JavaFX). If loading from - * file fails, we assume we have been given the name of a font which is passed - * directly to JavaFX for loading. A fallback font is used if this fails. - * - * When looking for files, this method will search for the file in a search path - * starting with the folder containing the provided Java class (using Java's - * standard {@link Class#getResourceAsStream(String)} system). The search path - * also includes ".." (parent directory) and "../fonts". - * - * The loaded font will be cached, so that additional calls with the same file - * name will not cause the file to be loaded again. - * - * If the font cannot be loaded, a default font will be substituted. You may - * check which font you got using {@link Font#getName()} or - * {@link Font#getFamily()}. - * - * @param name - * Name of a font, or relative path to the font file - * @param size - * Desired point size of the font - * @param clazz - * A class for finding files relative to - * @return A JavaFX font - */ - public static final Font findFont(String name, double size, Class clazz) { - if (name == null || size < 0) - throw new IllegalArgumentException(); + /** + * Load a font. + *

    + * Will first try to use the provided string as a file name, and load the font + * from a font file (should be in a format supported by JavaFX). If loading from + * file fails, we assume we have been given the name of a font which is passed + * directly to JavaFX for loading. A fallback font is used if this fails. + *

    + * When looking for files, this method will search for the file in a search path + * starting with the folder containing the provided Java class (using Java's + * standard {@link Class#getResourceAsStream(String)} system). The search path + * also includes ".." (parent directory) and "../fonts". + *

    + * The loaded font will be cached, so that additional calls with the same file + * name will not cause the file to be loaded again. + *

    + * If the font cannot be loaded, a default font will be substituted. You may + * check which font you got using {@link Font#getName()} or + * {@link Font#getFamily()}. + * + * @param name Name of a font, or relative path to the font file + * @param size Desired point size of the font + * @param clazz A class for finding files relative to + * @return A JavaFX font + */ + public static final Font findFont(String name, double size, Class clazz) { + if (name == null || size < 0) { + throw new IllegalArgumentException(); + } - if (loadedFonts.containsKey(name)) - return Font.font(loadedFonts.get(name), size); + if (loadedFonts.containsKey(name)) { + return Font.font(loadedFonts.get(name), size); + } - for (String path : searchPath) { - ClassLoader classloader = Thread.currentThread().getContextClassLoader(); - try (InputStream stream = classloader.getResourceAsStream(path + name)) { - Font font = Font.loadFont(stream, size); - if (font != null) { - loadedFonts.put(name, font.getName()); - // System.err.println("Found: " + font.getName()); - return font; - } - } catch (FileNotFoundException e) { - // we'll just try the next alternative in the search path - } catch (IOException e) { - e.printStackTrace(); - } - } + for (String path : searchPath) { + ClassLoader classloader = Thread.currentThread().getContextClassLoader(); + try (InputStream stream = classloader.getResourceAsStream(path + name)) { + Font font = Font.loadFont(stream, size); + if (font != null) { + loadedFonts.put(name, font.getName()); + // System.err.println("Found: " + font.getName()); + return font; + } + } catch (FileNotFoundException e) { + // we'll just try the next alternative in the search path + } catch (IOException e) { + e.printStackTrace(); + } + } - Font font = Font.font(name, size); - if (font == null) - font = Font.font(size); - if (font == null) - throw new RuntimeException("Even the default font seems to be unavailable – this shouldn't happen! :("); - if (font.getName().equals(Font.getDefault().getName())) { - System.err.println("TextFont: Default font '" + font.getName() + "' substituted for '" + name + "'"); - } - // System.err.println("Found: " + name + "=" + font.getName()); + Font font = Font.font(name, size); + if (font == null) { + font = Font.font(size); + } + if (font == null) { + throw new RuntimeException("Even the default font seems to be unavailable – this shouldn't happen! :("); + } + if (font.getName().equals(Font.getDefault().getName())) { + System.err.println("TextFont: Default font '" + font.getName() + "' substituted for '" + name + "'"); + } + // System.err.println("Found: " + name + "=" + font.getName()); - loadedFonts.put(name, name); - return font; - } + loadedFonts.put(name, name); + return font; + } - private Canvas tmpCanvas; - private final WritableImage img; + private Canvas tmpCanvas; + private final WritableImage img; - /** - * The JavaFX font - */ - private Font font; - /** Font size */ - private final double size; + /** + * The JavaFX font + */ + private Font font; + /** + * Font size + */ + private final double size; - /** - * Horizontal positioning of letters. - * - * Each letter should be approximately centered within its available - * square-shaped space. - */ - private final double xTranslate; + /** + * Horizontal positioning of letters. + *

    + * Each letter should be approximately centered within its available + * square-shaped space. + */ + private final double xTranslate; - /** - * Vertical positioning of letters. - * - * Each letter should be positioned on the baseline so that ascenders and - * descenders fall within its available square-shaped space. - */ - private final double yTranslate; + /** + * Vertical positioning of letters. + *

    + * Each letter should be positioned on the baseline so that ascenders and + * descenders fall within its available square-shaped space. + */ + private final double yTranslate; - /** - * Horizontal scaling factor (1.0 means no scaling) - * - * Most fonts are relatively tall and narrow, and need horizontal scaling to fit - * a square shape. - */ - private final double xScale; + /** + * Horizontal scaling factor (1.0 means no scaling) + *

    + * Most fonts are relatively tall and narrow, and need horizontal scaling to fit + * a square shape. + */ + private final double xScale; - /** - * Vertical scaling factor (1.0 means no scaling) - */ - private final double yScale; + /** + * Vertical scaling factor (1.0 means no scaling) + */ + private final double yScale; - /** - * Width and height of the square-shaped space each letter should fit within - */ - private double squareSize; + /** + * Width and height of the square-shaped space each letter should fit within + */ + private double squareSize; - private String fileName; + private String fileName; - SnapshotParameters snapshotParameters = new SnapshotParameters(); + SnapshotParameters snapshotParameters = new SnapshotParameters(); - { - snapshotParameters.setFill(Color.TRANSPARENT); - } + { + snapshotParameters.setFill(Color.TRANSPARENT); + } - /** - * Create a new TextFont. - * - *

    - * TextFonts are used for drawing text and character graphics. Each character is - * assumed to be uniform in size, fitting within a square with sides of length - * {@link #getSquareSize()}. The given font file should contain a monospaced - * font of a type suitable for JavaFX, such as OpenType or TrueType. - * - *

    - * Additional horizontal and vertical positioning and scaling can be used to - * make the font fit with the square-shaped concept. - * - *

    - * See {@link #setGraphicsContext(GraphicsContext)} for setting up the graphics - * context for writing with the font, or - * {@link #setGraphicsContext(GraphicsContext, double)} for extra on-the-fly - * horizontal scaling, e.g. to make half-width letters ("hires" mode). - * - * - * @param font - * Name of the font file. Will search for the file in the same folder - * as the TextFont class, as well as ".." and "../fonts". - * @param squareSize - * The width and height of a square defining the bounds of letters - * @param xTranslate - * Horizontal positioning of letters - * @param yTranslate - * Vertical positioning of letters - * @param xScale - * Horizontal scaling factor - * @param yScale - * Vertical scaling factor - */ - public TextFont(Font font, double squareSize, double xTranslate, double yTranslate, double xScale, double yScale) { - super(); - this.fileName = font.getName(); - this.font = font; - this.size = font.getSize(); - this.squareSize = squareSize; - this.xTranslate = xTranslate; - this.yTranslate = yTranslate; - this.xScale = xScale; - this.yScale = yScale; - this.img = new WritableImage((int) squareSize, (int) squareSize); - } + /** + * Create a new TextFont. + * + *

    + * TextFonts are used for drawing text and character graphics. Each character is + * assumed to be uniform in size, fitting within a square with sides of length + * {@link #getSquareSize()}. The given font file should contain a monospaced + * font of a type suitable for JavaFX, such as OpenType or TrueType. + * + *

    + * Additional horizontal and vertical positioning and scaling can be used to + * make the font fit with the square-shaped concept. + * + *

    + * See {@link #setGraphicsContext(GraphicsContext)} for setting up the graphics + * context for writing with the font, or + * {@link #setGraphicsContext(GraphicsContext, double)} for extra on-the-fly + * horizontal scaling, e.g. to make half-width letters ("hires" mode). + * + * @param font Name of the font file. Will search for the file in the same folder + * as the TextFont class, as well as ".." and "../fonts". + * @param squareSize The width and height of a square defining the bounds of letters + * @param xTranslate Horizontal positioning of letters + * @param yTranslate Vertical positioning of letters + * @param xScale Horizontal scaling factor + * @param yScale Vertical scaling factor + */ + public TextFont(Font font, double squareSize, double xTranslate, double yTranslate, double xScale, double yScale) { + super(); + this.fileName = font.getName(); + this.font = font; + this.size = font.getSize(); + this.squareSize = squareSize; + this.xTranslate = xTranslate; + this.yTranslate = yTranslate; + this.xScale = xScale; + this.yScale = yScale; + this.img = new WritableImage((int) squareSize, (int) squareSize); + } - /** - * Create a new TextFont. - * - *

    - * TextFonts are used for drawing text and character graphics. Each character is - * assumed to be uniform in size, fitting within a square with sides of length - * {@link #getSquareSize()}. The given font file should contain a monospaced - * font of a type suitable for JavaFX, such as OpenType or TrueType. - * - *

    - * Additional horizontal and vertical positioning and scaling can be used to - * make the font fit with the square-shaped concept. - * - *

    - * See {@link #setGraphicsContext(GraphicsContext)} for setting up the graphics - * context for writing with the font, or - * {@link #setGraphicsContext(GraphicsContext, double)} for extra on-the-fly - * horizontal scaling, e.g. to make half-width letters ("hires" mode). - * - * - * @param size - * Point size of the font. - * @param squareSize - * The width and height of a square defining the bounds of letters - * @param xTranslate - * Horizontal positioning of letters - * @param yTranslate - * Vertical positioning of letters - * @param xScale - * Horizontal scaling factor - * @param yScale - * Vertical scaling factor - */ - public TextFont(String fileName, double size, double squareSize, double xTranslate, double yTranslate, - double xScale, double yScale) { - super(); - this.fileName = fileName; - this.font = findFont(fileName, size); - this.size = size; - this.squareSize = squareSize; - this.xTranslate = xTranslate; - this.yTranslate = yTranslate; - this.xScale = xScale; - this.yScale = yScale; - this.img = new WritableImage((int) squareSize, (int) squareSize); - } + /** + * Create a new TextFont. + * + *

    + * TextFonts are used for drawing text and character graphics. Each character is + * assumed to be uniform in size, fitting within a square with sides of length + * {@link #getSquareSize()}. The given font file should contain a monospaced + * font of a type suitable for JavaFX, such as OpenType or TrueType. + * + *

    + * Additional horizontal and vertical positioning and scaling can be used to + * make the font fit with the square-shaped concept. + * + *

    + * See {@link #setGraphicsContext(GraphicsContext)} for setting up the graphics + * context for writing with the font, or + * {@link #setGraphicsContext(GraphicsContext, double)} for extra on-the-fly + * horizontal scaling, e.g. to make half-width letters ("hires" mode). + * + * @param size Point size of the font. + * @param squareSize The width and height of a square defining the bounds of letters + * @param xTranslate Horizontal positioning of letters + * @param yTranslate Vertical positioning of letters + * @param xScale Horizontal scaling factor + * @param yScale Vertical scaling factor + */ + public TextFont(String fileName, double size, double squareSize, double xTranslate, double yTranslate, + double xScale, double yScale) { + super(); + this.fileName = fileName; + this.font = findFont(fileName, size); + this.size = size; + this.squareSize = squareSize; + this.xTranslate = xTranslate; + this.yTranslate = yTranslate; + this.xScale = xScale; + this.yScale = yScale; + this.img = new WritableImage((int) squareSize, (int) squareSize); + } - /** - * Create a new TextFont. - * - *

    - * TextFonts are used for drawing text and character graphics. Each character is - * assumed to be uniform in size, fitting within a square with sides of length - * {@link #getSquareSize()}. The given font file should contain a monospaced - * font of a type suitable for JavaFX, such as OpenType or TrueType. - * - *

    - * Additional horizontal and vertical positioning and scaling can be used to - * make the font fit with the square-shaped concept. - * - *

    - * See {@link #setGraphicsContext(GraphicsContext)} for setting up the graphics - * context for writing with the font, or - * {@link #setGraphicsContext(GraphicsContext, double)} for extra on-the-fly - * horizontal scaling, e.g. to make half-width letters ("hires" mode). - * - * - * @param size - * Point size of the font. - * @param squareSize - * The width and height of a square defining the bounds of letters - * @param xTranslate - * Horizontal positioning of letters - * @param yTranslate - * Vertical positioning of letters - * @param xScale - * Horizontal scaling factor - * @param yScale - * Vertical scaling factor - * @param deferLoading - * True if the font file shouldn't be loaded before the font is - * actually used - */ - public TextFont(String fileName, double size, double squareSize, double xTranslate, double yTranslate, - double xScale, double yScale, boolean deferLoading) { - super(); - this.fileName = fileName; - this.font = deferLoading ? null : findFont(fileName, size); - this.size = size; - this.squareSize = squareSize; - this.xTranslate = xTranslate; - this.yTranslate = yTranslate; - this.xScale = xScale; - this.yScale = yScale; - this.img = new WritableImage((int) squareSize, (int) squareSize); - } + /** + * Create a new TextFont. + * + *

    + * TextFonts are used for drawing text and character graphics. Each character is + * assumed to be uniform in size, fitting within a square with sides of length + * {@link #getSquareSize()}. The given font file should contain a monospaced + * font of a type suitable for JavaFX, such as OpenType or TrueType. + * + *

    + * Additional horizontal and vertical positioning and scaling can be used to + * make the font fit with the square-shaped concept. + * + *

    + * See {@link #setGraphicsContext(GraphicsContext)} for setting up the graphics + * context for writing with the font, or + * {@link #setGraphicsContext(GraphicsContext, double)} for extra on-the-fly + * horizontal scaling, e.g. to make half-width letters ("hires" mode). + * + * @param size Point size of the font. + * @param squareSize The width and height of a square defining the bounds of letters + * @param xTranslate Horizontal positioning of letters + * @param yTranslate Vertical positioning of letters + * @param xScale Horizontal scaling factor + * @param yScale Vertical scaling factor + * @param deferLoading True if the font file shouldn't be loaded before the font is + * actually used + */ + public TextFont(String fileName, double size, double squareSize, double xTranslate, double yTranslate, + double xScale, double yScale, boolean deferLoading) { + super(); + this.fileName = fileName; + this.font = deferLoading ? null : findFont(fileName, size); + this.size = size; + this.squareSize = squareSize; + this.xTranslate = xTranslate; + this.yTranslate = yTranslate; + this.xScale = xScale; + this.yScale = yScale; + this.img = new WritableImage((int) squareSize, (int) squareSize); + } - /** - * Create a copy of this font, with the given adjustments to the translation and - * scaling. - * - * @param deltaXTranslate - * @param deltaYTranslate - * @param deltaXScale - * @param deltaYScale - * @return - */ - public TextFont adjust(double size, double deltaXTranslate, double deltaYTranslate, double deltaXScale, - double deltaYScale) { - if (size == 0.0) { - return new TextFont(fileName, this.size, squareSize, xTranslate + deltaXTranslate, - yTranslate + deltaYTranslate, xScale + deltaXScale, yScale + deltaYScale); - } else { - return new TextFont(fileName, this.size + size, squareSize, xTranslate + deltaXTranslate, - yTranslate + deltaYTranslate, xScale + deltaXScale, yScale + deltaYScale); - } + /** + * Create a copy of this font, with the given adjustments to the translation and + * scaling. + * + * @param deltaXTranslate + * @param deltaYTranslate + * @param deltaXScale + * @param deltaYScale + * @return + */ + public TextFont adjust(double size, double deltaXTranslate, double deltaYTranslate, double deltaXScale, + double deltaYScale) { + if (size == 0.0) { + return new TextFont(fileName, this.size, squareSize, xTranslate + deltaXTranslate, + yTranslate + deltaYTranslate, xScale + deltaXScale, yScale + deltaYScale); + } else { + return new TextFont(fileName, this.size + size, squareSize, xTranslate + deltaXTranslate, + yTranslate + deltaYTranslate, xScale + deltaXScale, yScale + deltaYScale); + } - } + } - /** - * Draw the given text at position (0,0). - * - * The ctx should normally be translated to the appropriate text - * position before calling this method. - * - * Text will be clipped so each character fits its expected square-shaped area, - * and the area will be cleared to transparency before drwaing. - * - * The graphics context's current path will be overwritten. - * - * @param ctx - * a grapics context - * @param text - * string to be printed - */ - public void drawText(GraphicsContext ctx, String text) { - textAt(ctx, 0.0, 0.0, text, 1.0, true, true, ctx.getFill(), null, 0, null); - } + /** + * Draw the given text at position (0,0). + *

    + * The ctx should normally be translated to the appropriate text + * position before calling this method. + *

    + * Text will be clipped so each character fits its expected square-shaped area, + * and the area will be cleared to transparency before drwaing. + *

    + * The graphics context's current path will be overwritten. + * + * @param ctx a grapics context + * @param text string to be printed + */ + public void drawText(GraphicsContext ctx, String text) { + textAt(ctx, 0.0, 0.0, text, 1.0, true, true, ctx.getFill(), null, 0, null); + } - /** - * Draw the given text at position (0,0), with horizontal scaling. - * - * The ctx should normally be translated to the appropriate text - * position before calling this method. - * - * Text will be clipped so each character fits its expected square-shaped area, - * and the area will be cleared to transparency before drwaing. - * - * The graphics context's current path will be overwritten. - * - * @param ctx - * a grapics context - * @param text - * string to be printed - * @param xScaleFactor - * a horizontal scaling factor - */ - public void drawText(GraphicsContext ctx, String text, double xScaleFactor) { - textAt(ctx, 0.0, 0.0, text, xScaleFactor, true, false, ctx.getFill(), null, 0, null); - } + /** + * Draw the given text at position (0,0), with horizontal scaling. + *

    + * The ctx should normally be translated to the appropriate text + * position before calling this method. + *

    + * Text will be clipped so each character fits its expected square-shaped area, + * and the area will be cleared to transparency before drwaing. + *

    + * The graphics context's current path will be overwritten. + * + * @param ctx a grapics context + * @param text string to be printed + * @param xScaleFactor a horizontal scaling factor + */ + public void drawText(GraphicsContext ctx, String text, double xScaleFactor) { + textAt(ctx, 0.0, 0.0, text, xScaleFactor, true, false, ctx.getFill(), null, 0, null); + } - /** - * Draw the given text at position (x,y). - * - * Text will be clipped so each character fits its expected square-shaped area, - * and the area will be cleared to transparency before drwaing. - * - * The graphics context's current path will be overwritten. - * - * @param ctx - * a grapics context - * @param x - * X-position of the lower left corner of the text - * @param y - * Y-position of the lower left corner of the text - * @param text - * string to be printed - */ - public void drawTextAt(GraphicsContext ctx, double x, double y, String text) { - textAt(ctx, x, y, text, 1.0, true, false, ctx.getFill(), null, 0, null); - } + /** + * Draw the given text at position (x,y). + *

    + * Text will be clipped so each character fits its expected square-shaped area, + * and the area will be cleared to transparency before drwaing. + *

    + * The graphics context's current path will be overwritten. + * + * @param ctx a grapics context + * @param x X-position of the lower left corner of the text + * @param y Y-position of the lower left corner of the text + * @param text string to be printed + */ + public void drawTextAt(GraphicsContext ctx, double x, double y, String text) { + textAt(ctx, x, y, text, 1.0, true, false, ctx.getFill(), null, 0, null); + } - /** - * Draw the given text at position (x, y), with horizontal scaling. - * - * The area will be cleared to transparency before drawing. - * - * The graphics context's current path will be overwritten. - * - * @param ctx - * a graphics context - * @param x - * X-position of the lower left corner of the text - * @param y - * Y-position of the lower left corner of the text - * @param text - * string to be printed - * @param xScaleFactor - * a horizontal scaling factor - */ - public void drawTextAt(GraphicsContext ctx, double x, double y, String text, double xScaleFactor, int mode, - Paint bg) { - textAt(ctx, x, y, text, xScaleFactor, true, false, ctx.getFill(), null, mode, bg); - } + /** + * Draw the given text at position (x, y), with horizontal scaling. + *

    + * The area will be cleared to transparency before drawing. + *

    + * The graphics context's current path will be overwritten. + * + * @param ctx a graphics context + * @param x X-position of the lower left corner of the text + * @param y Y-position of the lower left corner of the text + * @param text string to be printed + * @param xScaleFactor a horizontal scaling factor + */ + public void drawTextAt(GraphicsContext ctx, double x, double y, String text, double xScaleFactor, int mode, + Paint bg) { + textAt(ctx, x, y, text, xScaleFactor, true, false, ctx.getFill(), null, mode, bg); + } - /** - * Draw the given text at position (x, y), with horizontal scaling. - * - * The area will not be cleared to transparency before drawing. - * - * The graphics context's current path will be overwritten. - * - * @param ctx - * a graphics context - * @param x - * X-position of the lower left corner of the text - * @param y - * Y-position of the lower left corner of the text - * @param text - * string to be printed - * @param xScaleFactor - * a horizontal scaling factor - */ - public void drawTextNoClearAt(GraphicsContext ctx, double x, double y, String text, double xScaleFactor, int mode, - Paint bg) { - textAt(ctx, x, y, text, xScaleFactor, false, false, ctx.getFill(), null, mode, bg); - } + /** + * Draw the given text at position (x, y), with horizontal scaling. + *

    + * The area will not be cleared to transparency before drawing. + *

    + * The graphics context's current path will be overwritten. + * + * @param ctx a graphics context + * @param x X-position of the lower left corner of the text + * @param y Y-position of the lower left corner of the text + * @param text string to be printed + * @param xScaleFactor a horizontal scaling factor + */ + public void drawTextNoClearAt(GraphicsContext ctx, double x, double y, String text, double xScaleFactor, int mode, + Paint bg) { + textAt(ctx, x, y, text, xScaleFactor, false, false, ctx.getFill(), null, mode, bg); + } - private void fakeBlockElement(GraphicsContext ctx, String text, double xScaleFactor) { - text.codePoints().forEach((int c) -> { - // System.out.printf("%s %x%n", text, c); - if (c >= 0x2596 && c <= 0x259f) { // quadrants - int bits = BlocksAndBoxes.unicodeBlocksString.indexOf(c); - if ((bits & 1) > 0) { // lower right - ctx.fillRect(squareSize * xScaleFactor / 2, -squareSize / 2, (squareSize / 2) * xScaleFactor, - squareSize / 2); - } - if ((bits & 2) > 0) { // lower left - ctx.fillRect(0, -squareSize / 2, (squareSize / 2) * xScaleFactor, squareSize / 2); - } - if ((bits & 4) > 0) { // upper right - ctx.fillRect(squareSize * xScaleFactor / 2, -squareSize, (squareSize / 2) * xScaleFactor, - squareSize / 2); - } - if ((bits & 8) > 0) { // upper left - ctx.fillRect(0, -squareSize, (squareSize / 2) * xScaleFactor, squareSize / 2); - } - } else if (c == 0x2580) { // upper half - ctx.fillRect(0, -squareSize, (squareSize) * xScaleFactor, squareSize / 2); - } else if (c > 0x2580 && c <= 0x2588) { // x/8 block - int height = c - 0x2580; - ctx.fillRect(0, -(height * squareSize) / 8, (squareSize) * xScaleFactor, (height * squareSize) / 8); - } else if (c >= 0x2589 && c <= 0x258f) { // x/8 block - int width = 8 - (c - 0x2588); - ctx.fillRect(0, -squareSize, ((width * squareSize) / 8) * xScaleFactor, squareSize); - } else if (c == 0x2590) { // right half - ctx.fillRect(squareSize * xScaleFactor / 2, -squareSize, (squareSize / 2) * xScaleFactor, squareSize); - } else if (c == 0x2591) { // light shade - ctx.save(); - ctx.setGlobalAlpha(0.25); - ctx.fillRect(0, -squareSize, (squareSize) * xScaleFactor, squareSize); - ctx.restore(); - } else if (c == 0x2592) { // medium shade - ctx.save(); - ctx.setGlobalAlpha(0.5); - ctx.fillRect(0, -squareSize, (squareSize) * xScaleFactor, squareSize); - ctx.restore(); - } else if (c == 0x2593) { // dark shade - ctx.save(); - ctx.setGlobalAlpha(0.75); - ctx.fillRect(0, -squareSize, (squareSize) * xScaleFactor, squareSize); - ctx.restore(); - } else if (c == 0x2594) { // upper eighth - ctx.fillRect(0, -squareSize, (squareSize) * xScaleFactor, squareSize / 8); - } else if (c == 0x2595) { // right eighth - ctx.fillRect(((7 * squareSize) / 8) * xScaleFactor, -squareSize, (squareSize / 8) * xScaleFactor, - squareSize); - } else if (c == 0x2571) { - ctx.save(); - ctx.setLineWidth(2.0); - ctx.strokeLine(0, 0, squareSize * xScaleFactor, -squareSize); - ctx.restore(); - } else if (c == 0x2572) { - ctx.save(); - ctx.setLineWidth(2.0); - ctx.strokeLine(0, -squareSize, squareSize * xScaleFactor, 0); - ctx.restore(); - } else if (c == 0x2573) { - ctx.save(); - ctx.setLineWidth(2.0); - ctx.strokeLine(0, 0, squareSize * xScaleFactor, -squareSize); - ctx.strokeLine(0, -squareSize, squareSize * xScaleFactor, 0); - ctx.restore(); - } else if (c >= 0x2500 && c <= 0x257f) { - ctx.save(); - ctx.setLineCap(StrokeLineCap.BUTT); - String spec = boxDrawingShapes[c - 0x2500]; - int i = 0; - double extraThickness = 0.0; - if (Character.isDigit(spec.charAt(i))) { - extraThickness = setContextFromChar(ctx, spec.charAt(i++)); - } - char s; - double hThickness = Math.max(setContextFromChar(ctx, spec.charAt(i)), - setContextFromChar(ctx, spec.charAt(i + 1))) + extraThickness; - double vThickness = Math.max(setContextFromChar(ctx, spec.charAt(i + 2)), - setContextFromChar(ctx, spec.charAt(i + 3))) + extraThickness; - if (c >= 0x2550 || spec.charAt(i) != spec.charAt(i + 1)) { - s = spec.charAt(i++); - setContextFromChar(ctx, s); - strokeHalfLine(ctx, xScaleFactor, -1, 0, s, vThickness); - s = spec.charAt(i++); - setContextFromChar(ctx, s); - strokeHalfLine(ctx, xScaleFactor, 1, 0, s, vThickness); - } else { - s = spec.charAt(i++); - s = spec.charAt(i++); - setContextFromChar(ctx, s); - strokeFullLine(ctx, xScaleFactor, 1, 0, s); - } - if (c >= 0x2550 || spec.charAt(i) != spec.charAt(i + 1)) { - s = spec.charAt(i++); - setContextFromChar(ctx, s); - strokeHalfLine(ctx, xScaleFactor, 0, -1, s, hThickness); - s = spec.charAt(i++); - setContextFromChar(ctx, s); - strokeHalfLine(ctx, xScaleFactor, 0, 1, s, hThickness); - } else { - s = spec.charAt(i++); - s = spec.charAt(i++); - setContextFromChar(ctx, s); - strokeFullLine(ctx, xScaleFactor, 0, 1, s); + private void fakeBlockElement(GraphicsContext ctx, String text, double xScaleFactor) { + text.codePoints().forEach((int c) -> { + // System.out.printf("%s %x%n", text, c); + if (c >= 0x2596 && c <= 0x259f) { // quadrants + int bits = BlocksAndBoxes.unicodeBlocksString.indexOf(c); + if ((bits & 1) > 0) { // lower right + ctx.fillRect(squareSize * xScaleFactor / 2, -squareSize / 2, (squareSize / 2) * xScaleFactor, + squareSize / 2); + } + if ((bits & 2) > 0) { // lower left + ctx.fillRect(0, -squareSize / 2, (squareSize / 2) * xScaleFactor, squareSize / 2); + } + if ((bits & 4) > 0) { // upper right + ctx.fillRect(squareSize * xScaleFactor / 2, -squareSize, (squareSize / 2) * xScaleFactor, + squareSize / 2); + } + if ((bits & 8) > 0) { // upper left + ctx.fillRect(0, -squareSize, (squareSize / 2) * xScaleFactor, squareSize / 2); + } + } else if (c == 0x2580) { // upper half + ctx.fillRect(0, -squareSize, (squareSize) * xScaleFactor, squareSize / 2); + } else if (c > 0x2580 && c <= 0x2588) { // x/8 block + int height = c - 0x2580; + ctx.fillRect(0, -(height * squareSize) / 8, (squareSize) * xScaleFactor, (height * squareSize) / 8); + } else if (c >= 0x2589 && c <= 0x258f) { // x/8 block + int width = 8 - (c - 0x2588); + ctx.fillRect(0, -squareSize, ((width * squareSize) / 8) * xScaleFactor, squareSize); + } else if (c == 0x2590) { // right half + ctx.fillRect(squareSize * xScaleFactor / 2, -squareSize, (squareSize / 2) * xScaleFactor, squareSize); + } else if (c == 0x2591) { // light shade + ctx.save(); + ctx.setGlobalAlpha(0.25); + ctx.fillRect(0, -squareSize, (squareSize) * xScaleFactor, squareSize); + ctx.restore(); + } else if (c == 0x2592) { // medium shade + ctx.save(); + ctx.setGlobalAlpha(0.5); + ctx.fillRect(0, -squareSize, (squareSize) * xScaleFactor, squareSize); + ctx.restore(); + } else if (c == 0x2593) { // dark shade + ctx.save(); + ctx.setGlobalAlpha(0.75); + ctx.fillRect(0, -squareSize, (squareSize) * xScaleFactor, squareSize); + ctx.restore(); + } else if (c == 0x2594) { // upper eighth + ctx.fillRect(0, -squareSize, (squareSize) * xScaleFactor, squareSize / 8); + } else if (c == 0x2595) { // right eighth + ctx.fillRect(((7 * squareSize) / 8) * xScaleFactor, -squareSize, (squareSize / 8) * xScaleFactor, + squareSize); + } else if (c == 0x2571) { + ctx.save(); + ctx.setLineWidth(2.0); + ctx.strokeLine(0, 0, squareSize * xScaleFactor, -squareSize); + ctx.restore(); + } else if (c == 0x2572) { + ctx.save(); + ctx.setLineWidth(2.0); + ctx.strokeLine(0, -squareSize, squareSize * xScaleFactor, 0); + ctx.restore(); + } else if (c == 0x2573) { + ctx.save(); + ctx.setLineWidth(2.0); + ctx.strokeLine(0, 0, squareSize * xScaleFactor, -squareSize); + ctx.strokeLine(0, -squareSize, squareSize * xScaleFactor, 0); + ctx.restore(); + } else if (c >= 0x2500 && c <= 0x257f) { + ctx.save(); + ctx.setLineCap(StrokeLineCap.BUTT); + String spec = boxDrawingShapes[c - 0x2500]; + int i = 0; + double extraThickness = 0.0; + if (Character.isDigit(spec.charAt(i))) { + extraThickness = setContextFromChar(ctx, spec.charAt(i++)); + } + char s; + double hThickness = Math.max(setContextFromChar(ctx, spec.charAt(i)), + setContextFromChar(ctx, spec.charAt(i + 1))) + extraThickness; + double vThickness = Math.max(setContextFromChar(ctx, spec.charAt(i + 2)), + setContextFromChar(ctx, spec.charAt(i + 3))) + extraThickness; + if (c >= 0x2550 || spec.charAt(i) != spec.charAt(i + 1)) { + s = spec.charAt(i++); + setContextFromChar(ctx, s); + strokeHalfLine(ctx, xScaleFactor, -1, 0, s, vThickness); + s = spec.charAt(i++); + setContextFromChar(ctx, s); + strokeHalfLine(ctx, xScaleFactor, 1, 0, s, vThickness); + } else { + s = spec.charAt(i++); + s = spec.charAt(i++); + setContextFromChar(ctx, s); + strokeFullLine(ctx, xScaleFactor, 1, 0, s); + } + if (c >= 0x2550 || spec.charAt(i) != spec.charAt(i + 1)) { + s = spec.charAt(i++); + setContextFromChar(ctx, s); + strokeHalfLine(ctx, xScaleFactor, 0, -1, s, hThickness); + s = spec.charAt(i++); + setContextFromChar(ctx, s); + strokeHalfLine(ctx, xScaleFactor, 0, 1, s, hThickness); + } else { + s = spec.charAt(i++); + s = spec.charAt(i++); + setContextFromChar(ctx, s); + strokeFullLine(ctx, xScaleFactor, 0, 1, s); - } - ctx.restore(); - } - }); + } + ctx.restore(); + } + }); - } + } - /** - * @return the font - */ - public Font getFont() { - if (font == null) - font = findFont(fileName, size); - return font; - } + /** + * @return the font + */ + public Font getFont() { + if (font == null) { + font = findFont(fileName, size); + } + return font; + } - /** - * @return the size - */ - public double getSize() { - return size; - } + /** + * @return the size + */ + public double getSize() { + return size; + } - /** - * Width and height of the square-shaped space each letter should fit within - * - * @return the squareSize - */ - public double getSquareSize() { - return squareSize; - } + /** + * Width and height of the square-shaped space each letter should fit within + * + * @return the squareSize + */ + public double getSquareSize() { + return squareSize; + } - private Canvas getTmpCanvas() { - if (tmpCanvas == null) { - tmpCanvas = new Canvas(squareSize, squareSize); - } else { - tmpCanvas.getGraphicsContext2D().clearRect(0, 0, squareSize, squareSize); - } - return tmpCanvas; - } + private Canvas getTmpCanvas() { + if (tmpCanvas == null) { + tmpCanvas = new Canvas(squareSize, squareSize); + } else { + tmpCanvas.getGraphicsContext2D().clearRect(0, 0, squareSize, squareSize); + } + return tmpCanvas; + } - /** - * Horizontal scaling factor (1.0 means no scaling) - * - * Most fonts are relatively tall and narrow, and need horizontal scaling to fit - * a square shape. - * - * @return the xScale - */ - public double getxScale() { - return xScale; - } + /** + * Horizontal scaling factor (1.0 means no scaling) + *

    + * Most fonts are relatively tall and narrow, and need horizontal scaling to fit + * a square shape. + * + * @return the xScale + */ + public double getxScale() { + return xScale; + } - /** - * Horizontal positioning of letters. - * - * Each letter should be approximately centered within its available - * square-shaped space. - * - * @return the xTranslate - */ - public double getxTranslate() { - return xTranslate; - } + /** + * Horizontal positioning of letters. + *

    + * Each letter should be approximately centered within its available + * square-shaped space. + * + * @return the xTranslate + */ + public double getxTranslate() { + return xTranslate; + } - /** - * Vertical scaling factor (1.0 means no scaling) - * - * @return the yScale - */ - public double getyScale() { - return yScale; - } + /** + * Vertical scaling factor (1.0 means no scaling) + * + * @return the yScale + */ + public double getyScale() { + return yScale; + } - /** - * /** Vertical positioning of letters. - * - * Each letter should be positioned on the baseline so that ascenders and - * descenders fall within its available square-shaped space. - * - * @return the yTranslate - */ - public double getyTranslate() { - return yTranslate; - } + /** + * /** Vertical positioning of letters. + *

    + * Each letter should be positioned on the baseline so that ascenders and + * descenders fall within its available square-shaped space. + * + * @return the yTranslate + */ + public double getyTranslate() { + return yTranslate; + } - private double setContextFromChar(GraphicsContext ctx, char c) { - switch (c) { - case '0': - return -2 * thin; - case '2': - ctx.setLineDashes(14.75, 2.5); - break; - case '3': - ctx.setLineDashes(9, 2.5); - break; - case '4': - ctx.setLineDashes(6.125, 2.5); - break; - case '.': - return 0.0; - case '-': - case 's': - ctx.setLineWidth(thin); - return thin; - case '*': - ctx.setLineWidth(thick); - return thick; - case '=': - case 'N': - case 'M': - case 'z': - ctx.setLineWidth(thin); - return thin; - } - return 0.0; - } + private double setContextFromChar(GraphicsContext ctx, char c) { + switch (c) { + case '0': + return -2 * thin; + case '2': + ctx.setLineDashes(14.75, 2.5); + break; + case '3': + ctx.setLineDashes(9, 2.5); + break; + case '4': + ctx.setLineDashes(6.125, 2.5); + break; + case '.': + return 0.0; + case '-': + case 's': + ctx.setLineWidth(thin); + return thin; + case '*': + ctx.setLineWidth(thick); + return thick; + case '=': + case 'N': + case 'M': + case 'z': + ctx.setLineWidth(thin); + return thin; + } + return 0.0; + } - /** - * Set up a graphics context for drawing with this font. - * - * Caller should call {@link GraphicsContext#save()} first, and then - * {@link GraphicsContext#restore()} afterwards, to clean up adjustments to the - * transformation matrix (i.e., translation, scaling). - * - * The GraphicsContext should be translated to the coordinates where the text - * should appear before calling this method, since this method will - * modify the coordinate system. - * - * @param ctx - * A GraphicsContext - */ - public void setGraphicsContext(GraphicsContext ctx) { - ctx.setFont(getFont()); - ctx.translate(xTranslate, yTranslate); - ctx.scale(xScale, yScale); - } + /** + * Set up a graphics context for drawing with this font. + *

    + * Caller should call {@link GraphicsContext#save()} first, and then + * {@link GraphicsContext#restore()} afterwards, to clean up adjustments to the + * transformation matrix (i.e., translation, scaling). + *

    + * The GraphicsContext should be translated to the coordinates where the text + * should appear before calling this method, since this method will + * modify the coordinate system. + * + * @param ctx A GraphicsContext + */ + public void setGraphicsContext(GraphicsContext ctx) { + ctx.setFont(getFont()); + ctx.translate(xTranslate, yTranslate); + ctx.scale(xScale, yScale); + } - /** - * Set up a graphics context for drawing with this font. - * - * Caller should call {@link GraphicsContext#save()} first, and then - * {@link GraphicsContext#restore()} afterwards, to clean up adjustments to the - * transformation matrix (i.e., translation, scaling). - * - * The GraphicsContext should be translated to the coordinates where the text - * should appear before calling this method, since this method will - * modify the coordinate system. - * - * @param ctx - * A GraphicsContext - * @param xScaleFactor - * Additional horizontal scaling, normally 0.5 (for half-width - * characters) - */ - public void setGraphicsContext(GraphicsContext ctx, double xScaleFactor) { - ctx.setFont(getFont()); - ctx.translate(xTranslate * xScaleFactor, yTranslate); - ctx.scale(xScale * xScaleFactor, yScale); - } + /** + * Set up a graphics context for drawing with this font. + *

    + * Caller should call {@link GraphicsContext#save()} first, and then + * {@link GraphicsContext#restore()} afterwards, to clean up adjustments to the + * transformation matrix (i.e., translation, scaling). + *

    + * The GraphicsContext should be translated to the coordinates where the text + * should appear before calling this method, since this method will + * modify the coordinate system. + * + * @param ctx A GraphicsContext + * @param xScaleFactor Additional horizontal scaling, normally 0.5 (for half-width + * characters) + */ + public void setGraphicsContext(GraphicsContext ctx, double xScaleFactor) { + ctx.setFont(getFont()); + ctx.translate(xTranslate * xScaleFactor, yTranslate); + ctx.scale(xScale * xScaleFactor, yScale); + } - private void strokeFullLine(GraphicsContext ctx, double xScaleFactor, double xDir, double yDir, char c) { - double x = squareSize * xScaleFactor / 2, y = -squareSize / 2; - if (c != '.') { - // System.out.printf("(%4.1f %4.1f %4.1f %4.1f) w=%1.4f%n", x * yDir, y * xDir, - // x * yDir + xDir * squareSize, - // y * xDir + yDir * squareSize, ctx.getLineWidth()); - if (c == '=') { - ctx.setLineWidth(2.0); - ctx.strokeLine((x - 2) * yDir, (y - 2) * xDir, (x - 2) * yDir + xDir * squareSize * xScaleFactor, - (y - 2) * xDir + yDir * -squareSize); - ctx.strokeLine((x + 2) * yDir, (y + 2) * xDir, (x + 2) * yDir + xDir * squareSize * xScaleFactor, - (y + 2) * xDir + yDir * -squareSize); - } else { - ctx.strokeLine(x * yDir, y * xDir, x * yDir + xDir * squareSize * xScaleFactor, - y * xDir + yDir * -squareSize); - } - } - } + private void strokeFullLine(GraphicsContext ctx, double xScaleFactor, double xDir, double yDir, char c) { + double x = squareSize * xScaleFactor / 2, y = -squareSize / 2; + if (c != '.') { + // System.out.printf("(%4.1f %4.1f %4.1f %4.1f) w=%1.4f%n", x * yDir, y * xDir, + // x * yDir + xDir * squareSize, + // y * xDir + yDir * squareSize, ctx.getLineWidth()); + if (c == '=') { + ctx.setLineWidth(2.0); + ctx.strokeLine((x - 2) * yDir, (y - 2) * xDir, (x - 2) * yDir + xDir * squareSize * xScaleFactor, + (y - 2) * xDir + yDir * -squareSize); + ctx.strokeLine((x + 2) * yDir, (y + 2) * xDir, (x + 2) * yDir + xDir * squareSize * xScaleFactor, + (y + 2) * xDir + yDir * -squareSize); + } else { + ctx.strokeLine(x * yDir, y * xDir, x * yDir + xDir * squareSize * xScaleFactor, + y * xDir + yDir * -squareSize); + } + } + } - private void strokeHalfLine(GraphicsContext ctx, double xScaleFactor, double xDir, double yDir, char c, - double otherWidth) { - if (c != '.') { - double factor = 1.0, dblFactor = 0.0; - if (c == 's' || c == 'z') - factor = -1; - if (c == 'N') - dblFactor = -2; - if (c == 'M') - dblFactor = 2; - double x = squareSize * xScaleFactor / 2, y = -squareSize / 2; - x -= xDir * factor * (otherWidth / 2); - y -= yDir * factor * (otherWidth / 2); + private void strokeHalfLine(GraphicsContext ctx, double xScaleFactor, double xDir, double yDir, char c, + double otherWidth) { + if (c != '.') { + double factor = 1.0, dblFactor = 0.0; + if (c == 's' || c == 'z') { + factor = -1; + } + if (c == 'N') { + dblFactor = -2; + } + if (c == 'M') { + dblFactor = 2; + } + double x = squareSize * xScaleFactor / 2, y = -squareSize / 2; + x -= xDir * factor * (otherWidth / 2); + y -= yDir * factor * (otherWidth / 2); - if (c == '=' || c == 'z' || c == 'N' || c == 'M') { - ctx.setLineWidth(2.0); - double x0 = x - 2 * yDir; - double y0 = y - 2 * xDir; - x0 += dblFactor * xDir * (otherWidth / 2); - y0 += dblFactor * yDir * (otherWidth / 2); - ctx.strokeLine(x0, y0, x0 + xDir * squareSize * xScaleFactor, y0 + yDir * squareSize); - double x1 = x + 2 * yDir; - double y1 = y + 2 * xDir; - x1 -= dblFactor * xDir * (otherWidth / 2); - y1 -= dblFactor * yDir * (otherWidth / 2); - ctx.strokeLine(x1, y1, x1 + xDir * squareSize * xScaleFactor, y1 + yDir * squareSize); - } else { - ctx.strokeLine(x, y, x + xDir * squareSize * xScaleFactor, y + yDir * squareSize); - } - } - } + if (c == '=' || c == 'z' || c == 'N' || c == 'M') { + ctx.setLineWidth(2.0); + double x0 = x - 2 * yDir; + double y0 = y - 2 * xDir; + x0 += dblFactor * xDir * (otherWidth / 2); + y0 += dblFactor * yDir * (otherWidth / 2); + ctx.strokeLine(x0, y0, x0 + xDir * squareSize * xScaleFactor, y0 + yDir * squareSize); + double x1 = x + 2 * yDir; + double y1 = y + 2 * xDir; + x1 -= dblFactor * xDir * (otherWidth / 2); + y1 -= dblFactor * yDir * (otherWidth / 2); + ctx.strokeLine(x1, y1, x1 + xDir * squareSize * xScaleFactor, y1 + yDir * squareSize); + } else { + ctx.strokeLine(x, y, x + xDir * squareSize * xScaleFactor, y + yDir * squareSize); + } + } + } - /** - * Draw text at the given position. - * - * For most cases, the simpler {@link #drawText(GraphicsContext, String)} or - * {@link #drawText(GraphicsContext, String, double)} will be easier to use. - * - * If clip is true, the graphics context's current path will be - * overwritten. - * - * @param ctx - * A GraphicsContext - * @param x - * X-position of the lower left corner of the text - * @param y - * Y-position of the lower left corner of the text - * @param text - * The text to be printed - * @param xScaleFactor - * Horizontal scaling factor, normally 1.0 (full width) or 0.5 (half - * width) - * @param clear - * True if the area should be cleared (to transparency) before - * drawing; normally true. - * @param clip - * True if the text drawing should be clipped to fit the expected - * printing area; normally true. - * @param fill - * True if the letter shapes should be filled; normally true. - * @param stroke - * True if the letter shapes should be stroked (outlined); normally - * false. - */ - public void textAt(GraphicsContext ctx, double x, double y, String text, double xScaleFactor, boolean clear, - boolean clip, Paint fill, Paint stroke, int mode, Paint bg) { - if ((mode & ATTR_BRIGHT) != 0) { - fill = fill instanceof Color ? ((Color) fill).deriveColor(0.0, 1.0, 3.0, 1.0) : fill; - stroke = stroke instanceof Color ? ((Color) stroke).deriveColor(0.0, 1.0, 3.0, 1.0) : stroke; - } - if ((mode & ATTR_FAINT) != 0) { - fill = fill instanceof Color ? ((Color) fill).deriveColor(0.0, 1.0, 1.0, .5) : fill; - stroke = stroke instanceof Color ? ((Color) stroke).deriveColor(0.0, 1.0, 1.0, .5) : stroke; - } - GraphicsContext target = ctx; - int width = text.codePointCount(0, text.length()); - ctx.save(); // save 1 - ctx.setFill(fill); - ctx.setStroke(stroke); - ctx.translate(x, y); - if (clear && (mode & ATTR_INVERSE) == 0 && (mode & ATTR_OVERSTRIKE) == 0) { - ctx.clearRect(0 + 0.5, -squareSize + 0.5, squareSize * width * xScaleFactor - 1, squareSize - 1); - } - if (bg != null && bg != Color.TRANSPARENT) { - ctx.save(); // save 2 - ctx.setFill(bg); - ctx.fillRect(0, -squareSize, squareSize * width * xScaleFactor, squareSize); - ctx.restore(); // restore 2 - } - boolean drawIndirect = clip || (mode & (ATTR_INVERSE | ATTR_CLIP)) != 0; - Canvas tmpCanvas = getTmpCanvas(); - if (drawIndirect) { - target = tmpCanvas.getGraphicsContext2D(); - target.save(); // save 2 if drawIndirect - target.translate(0, squareSize); - target.setFill((mode & ATTR_INVERSE) != 0 ? Color.BLACK : fill); - target.setStroke((mode & ATTR_INVERSE) != 0 ? Color.BLACK : stroke); - } - if (text.length() > 0 && text.charAt(0) >= 0x2500 && text.charAt(0) <= 0x259f - && (mode & ATTR_NO_FAKE_CHARS) == 0) { - target.save(); // save 3 - target.setStroke(target.getFill()); - fakeBlockElement(target, text, xScaleFactor); - target.restore(); // restore 3 - } else { - target.save(); // save 3 - if ((mode & ATTR_ITALIC) != 0) { - target.translate(-0.2, 0); - target.transform(new Affine(Transform.shear(-0.2, 0))); - } - setGraphicsContext(target, xScaleFactor); - if (fill != null) - target.fillText(text, 0.0, 0.0); - if ((mode & ATTR_BOLD) != 0) { - // System.err.println("stroke2"); - target.save(); // save 4 - target.setLineWidth(thin); - target.setStroke(target.getFill()); - target.strokeText(text, 0.0, 0.0); - target.restore(); // restore 4 - } - if (stroke != null || (mode & ATTR_OUTLINE) != 0) { - // System.err.println("stroke2"); - target.setLineWidth(((mode & ATTR_BOLD) != 0) ? thin : thin / 2); - target.strokeText(text, 0.0, 0.0); - } - target.restore(); // restore 3 - } + /** + * Draw text at the given position. + *

    + * For most cases, the simpler {@link #drawText(GraphicsContext, String)} or + * {@link #drawText(GraphicsContext, String, double)} will be easier to use. + *

    + * If clip is true, the graphics context's current path will be + * overwritten. + * + * @param ctx A GraphicsContext + * @param x X-position of the lower left corner of the text + * @param y Y-position of the lower left corner of the text + * @param text The text to be printed + * @param xScaleFactor Horizontal scaling factor, normally 1.0 (full width) or 0.5 (half + * width) + * @param clear True if the area should be cleared (to transparency) before + * drawing; normally true. + * @param clip True if the text drawing should be clipped to fit the expected + * printing area; normally true. + * @param fill True if the letter shapes should be filled; normally true. + * @param stroke True if the letter shapes should be stroked (outlined); normally + * false. + */ + public void textAt(GraphicsContext ctx, double x, double y, String text, double xScaleFactor, boolean clear, + boolean clip, Paint fill, Paint stroke, int mode, Paint bg) { + if ((mode & ATTR_BRIGHT) != 0) { + fill = fill instanceof Color ? ((Color) fill).deriveColor(0.0, 1.0, 3.0, 1.0) : fill; + stroke = stroke instanceof Color ? ((Color) stroke).deriveColor(0.0, 1.0, 3.0, 1.0) : stroke; + } + if ((mode & ATTR_FAINT) != 0) { + fill = fill instanceof Color ? ((Color) fill).deriveColor(0.0, 1.0, 1.0, .5) : fill; + stroke = stroke instanceof Color ? ((Color) stroke).deriveColor(0.0, 1.0, 1.0, .5) : stroke; + } + GraphicsContext target = ctx; + int width = text.codePointCount(0, text.length()); + ctx.save(); // save 1 + ctx.setFill(fill); + ctx.setStroke(stroke); + ctx.translate(x, y); + if (clear && (mode & ATTR_INVERSE) == 0 && (mode & ATTR_OVERSTRIKE) == 0) { + ctx.clearRect(0 + 0.5, -squareSize + 0.5, squareSize * width * xScaleFactor - 1, squareSize - 1); + } + if (bg != null && bg != Color.TRANSPARENT) { + ctx.save(); // save 2 + ctx.setFill(bg); + ctx.fillRect(0, -squareSize, squareSize * width * xScaleFactor, squareSize); + ctx.restore(); // restore 2 + } + boolean drawIndirect = clip || (mode & (ATTR_INVERSE | ATTR_CLIP)) != 0; + Canvas tmpCanvas = getTmpCanvas(); + if (drawIndirect) { + target = tmpCanvas.getGraphicsContext2D(); + target.save(); // save 2 if drawIndirect + target.translate(0, squareSize); + target.setFill((mode & ATTR_INVERSE) != 0 ? Color.BLACK : fill); + target.setStroke((mode & ATTR_INVERSE) != 0 ? Color.BLACK : stroke); + } + if (text.length() > 0 && text.charAt(0) >= 0x2500 && text.charAt(0) <= 0x259f + && (mode & ATTR_NO_FAKE_CHARS) == 0) { + target.save(); // save 3 + target.setStroke(target.getFill()); + fakeBlockElement(target, text, xScaleFactor); + target.restore(); // restore 3 + } else { + target.save(); // save 3 + if ((mode & ATTR_ITALIC) != 0) { + target.translate(-0.2, 0); + target.transform(new Affine(Transform.shear(-0.2, 0))); + } + setGraphicsContext(target, xScaleFactor); + if (fill != null) { + target.fillText(text, 0.0, 0.0); + } + if ((mode & ATTR_BOLD) != 0) { + // System.err.println("stroke2"); + target.save(); // save 4 + target.setLineWidth(thin); + target.setStroke(target.getFill()); + target.strokeText(text, 0.0, 0.0); + target.restore(); // restore 4 + } + if (stroke != null || (mode & ATTR_OUTLINE) != 0) { + // System.err.println("stroke2"); + target.setLineWidth(((mode & ATTR_BOLD) != 0) ? thin : thin / 2); + target.strokeText(text, 0.0, 0.0); + } + target.restore(); // restore 3 + } - if ((mode & ATTR_UNDERLINE) != 0) { - target.fillRect(0, yTranslate - 2, width * squareSize * xScaleFactor, thin); - } - if ((mode & ATTR_OVERLINE) != 0) { - target.fillRect(0, -squareSize + 2, width * squareSize * xScaleFactor, thin); - } - if ((mode & ATTR_LINE_THROUGH) != 0) { - target.fillRect(0, -squareSize / 2 + 2, width * squareSize * xScaleFactor, thin); - } + if ((mode & ATTR_UNDERLINE) != 0) { + target.fillRect(0, yTranslate - 2, width * squareSize * xScaleFactor, thin); + } + if ((mode & ATTR_OVERLINE) != 0) { + target.fillRect(0, -squareSize + 2, width * squareSize * xScaleFactor, thin); + } + if ((mode & ATTR_LINE_THROUGH) != 0) { + target.fillRect(0, -squareSize / 2 + 2, width * squareSize * xScaleFactor, thin); + } - if (drawIndirect) { - target.restore(); // restore 2 if drawIndirect - tmpCanvas.snapshot(snapshotParameters, img); - if ((mode & ATTR_INVERSE) != 0) { - fillInverse(ctx, img, 0, -squareSize); - } else - ctx.drawImage(img, 0, -squareSize); - } + if (drawIndirect) { + target.restore(); // restore 2 if drawIndirect + tmpCanvas.snapshot(snapshotParameters, img); + if ((mode & ATTR_INVERSE) != 0) { + fillInverse(ctx, img, 0, -squareSize); + } else { + ctx.drawImage(img, 0, -squareSize); + } + } - ctx.restore(); // restore 1 - } + ctx.restore(); // restore 1 + } } diff --git a/src/main/java/inf101/v18/gfx/textmode/TextFontAdjuster.java b/src/main/java/inf101/v18/gfx/textmode/TextFontAdjuster.java index 1efd738..33865ca 100644 --- a/src/main/java/inf101/v18/gfx/textmode/TextFontAdjuster.java +++ b/src/main/java/inf101/v18/gfx/textmode/TextFontAdjuster.java @@ -1,7 +1,6 @@ package inf101.v18.gfx.textmode; import inf101.v18.gfx.Screen; -import inf101.v18.gfx.textmode.Printer; import javafx.application.Application; import javafx.scene.input.KeyCode; import javafx.scene.input.KeyEvent; @@ -11,226 +10,227 @@ import javafx.stage.Stage; import java.util.Objects; public class TextFontAdjuster extends Application { - // private static final String FONT_NAME = "PetMe64.ttf"; - // new TextFont(FONT_NAME, 22.2, TextModes.CHAR_BOX_SIZE, 0.0, 0.0, 1.0, 1.0); - private static TextFontAdjuster demo; + // private static final String FONT_NAME = "PetMe64.ttf"; + // new TextFont(FONT_NAME, 22.2, TextModes.CHAR_BOX_SIZE, 0.0, 0.0, 1.0, 1.0); + private static TextFontAdjuster demo; - public static TextFontAdjuster getInstance() { - return demo; - } + public static TextFontAdjuster getInstance() { + return demo; + } - public static void main(String[] args) { - launch(args); - } + public static void main(String[] args) { + launch(args); + } - private TextFont textFont = Printer.FONT_SYMBOLA;// - // new TextFont("ZXSpectrum-7.otf", 22.00, TextMode.CHAR_BOX_SIZE, 3.1000, - // -3.8000, 1.0000, 1.0000, true); - private Screen screen; + private TextFont textFont = Printer.FONT_SYMBOLA;// + // new TextFont("ZXSpectrum-7.otf", 22.00, TextMode.CHAR_BOX_SIZE, 3.1000, + // -3.8000, 1.0000, 1.0000, true); + private Screen screen; - private boolean paused; - private Printer printer; + private boolean paused; + private Printer printer; - private boolean grid = true; + private boolean grid = true; - private double adjustAmount = 0.1; + private final double adjustAmount = 0.1; - private double adjustX(KeyCode code) { - switch (code) { - case LEFT: - return -1 * adjustAmount; - case RIGHT: - return 1 * adjustAmount; - default: - return 0; - } - } + private double adjustX(KeyCode code) { + switch (code) { + case LEFT: + return -1 * adjustAmount; + case RIGHT: + return 1 * adjustAmount; + default: + return 0; + } + } - private double adjustY(KeyCode code) { - switch (code) { - case UP: - return 1 * adjustAmount; - case DOWN: - return -1 * adjustAmount; - default: - return 0; - } - } + private double adjustY(KeyCode code) { + switch (code) { + case UP: + return 1 * adjustAmount; + case DOWN: + return -1 * adjustAmount; + default: + return 0; + } + } - private void drawBackgroundGrid() { - if (grid) { - printer.drawCharCells(); - /* - * painter.turnTo(0); for (int y = 0; y < printer.getPageHeight(); y++) { - * painter.jumpTo(0, y * printer.getCharHeight()); for (int x = 0; x < - * printer.getLineWidth(); x++) { painter.setInk( (x + y) % 2 == 0 ? - * Color.CORNFLOWERBLUE : Color.CORNFLOWERBLUE.brighter().brighter()); - * painter.fillRectangle(printer.getCharWidth(), printer.getCharHeight()); - * painter.jump(printer.getCharWidth()); } } - */ - } else { - screen.clearBackground(); - } - } + private void drawBackgroundGrid() { + if (grid) { + printer.drawCharCells(); + /* + * painter.turnTo(0); for (int y = 0; y < printer.getPageHeight(); y++) { + * painter.jumpTo(0, y * printer.getCharHeight()); for (int x = 0; x < + * printer.getLineWidth(); x++) { painter.setInk( (x + y) % 2 == 0 ? + * Color.CORNFLOWERBLUE : Color.CORNFLOWERBLUE.brighter().brighter()); + * painter.fillRectangle(printer.getCharWidth(), printer.getCharHeight()); + * painter.jump(printer.getCharWidth()); } } + */ + } else { + screen.clearBackground(); + } + } - private void printHelp() { - printer.moveTo(1, 1); - printer.setAutoScroll(false); - printer.println(" " + Printer.center("TextFontAdjuster", 36) + " "); - printer.println(" "); - printer.println(" "); - printer.println(" "); - printer.println("________________________________________"); - printer.println("Adjust letter parameters: "); - printer.println(" Font size: CTRL +, CTRL - "); - printer.println(" Position: LEFT, RIGHT, UP, DOWN "); - printer.println(" Scaling: CTRL-(LEFT, RIGHT, UP, DOWN)"); - printer.println("Commands / options (with CTRL key): "); - printer.println(" Hires (R) – Grid (G) – Fullscreen (F) "); - printer.println(" Help (H) – Quit (Q) "); - printer.println("Write text with any other key. "); - printer.println("_-*-_-*-_-*-_-*-_-*-_-*-_-*-_-*-_-*-_-*-"); - printer.println(" "); - printer.println("Sample text: "); - printer.println("the quick brown fox jumps over the lazy "); - printer.println(" dog, THE QUICK BROWN FOX JUMPS OVER THE"); - printer.println("LAZY DOG den vågale røde reven værer den"); - printer.println("sinte hunden DEN VÅGALE RØDE REVEN VÆRER"); - printer.println("DEN SINTE HUNDEN !\"#%&/()?,._-@£${[]}?|^"); + private void printHelp() { + printer.moveTo(1, 1); + printer.setAutoScroll(false); + printer.println(" " + Printer.center("TextFontAdjuster", 36) + " "); + printer.println(" "); + printer.println(" "); + printer.println(" "); + printer.println("________________________________________"); + printer.println("Adjust letter parameters: "); + printer.println(" Font size: CTRL +, CTRL - "); + printer.println(" Position: LEFT, RIGHT, UP, DOWN "); + printer.println(" Scaling: CTRL-(LEFT, RIGHT, UP, DOWN)"); + printer.println("Commands / options (with CTRL key): "); + printer.println(" Hires (R) – Grid (G) – Fullscreen (F) "); + printer.println(" Help (H) – Quit (Q) "); + printer.println("Write text with any other key. "); + printer.println("_-*-_-*-_-*-_-*-_-*-_-*-_-*-_-*-_-*-_-*-"); + printer.println(" "); + printer.println("Sample text: "); + printer.println("the quick brown fox jumps over the lazy "); + printer.println(" dog, THE QUICK BROWN FOX JUMPS OVER THE"); + printer.println("LAZY DOG den vågale røde reven værer den"); + printer.println("sinte hunden DEN VÅGALE RØDE REVEN VÆRER"); + printer.println("DEN SINTE HUNDEN !\"#%&/()?,._-@£${[]}?|^"); - // printer.print(" "); - printer.moveTo(1, 15); - printer.setAutoScroll(true); + // printer.print(" "); + printer.moveTo(1, 15); + printer.setAutoScroll(true); - } + } - private void printInfo() { - printer.moveTo(1, 3); - printer.println(String.format("Font: %s at %1.1fpt ", textFont.getFont().getName(), - textFont.getFont().getSize())); - printer.println(String.format(" xTr=%-1.1f yTr=%-1.1f xSc=%-1.1f ySc=%-1.1f ", textFont.getxTranslate(), - textFont.getyTranslate(), textFont.getxScale(), textFont.getyScale())); - System.out.printf("new TextFont(\"%s\", %1.2f, Printer.CHAR_HEIGHT, %1.4f, %1.4f, %1.4f, %1.4f)%n", - textFont.getFont().getName(), textFont.getSize(), textFont.getxTranslate(), textFont.getyTranslate(), - textFont.getxScale(), textFont.getyScale()); + private void printInfo() { + printer.moveTo(1, 3); + printer.println(String.format("Font: %s at %1.1fpt ", textFont.getFont().getName(), + textFont.getFont().getSize())); + printer.println(String.format(" xTr=%-1.1f yTr=%-1.1f xSc=%-1.1f ySc=%-1.1f ", textFont.getxTranslate(), + textFont.getyTranslate(), textFont.getxScale(), textFont.getyScale())); + System.out.printf("new TextFont(\"%s\", %1.2f, Printer.CHAR_HEIGHT, %1.4f, %1.4f, %1.4f, %1.4f)%n", + textFont.getFont().getName(), textFont.getSize(), textFont.getxTranslate(), textFont.getyTranslate(), + textFont.getxScale(), textFont.getyScale()); - printer.moveTo(1, 15); - } + printer.moveTo(1, 15); + } - private void setup() { - drawBackgroundGrid(); - printHelp(); - printInfo(); - } + private void setup() { + drawBackgroundGrid(); + printHelp(); + printInfo(); + } - @Override - public void start(Stage stage) { - demo = this; + @Override + public void start(Stage stage) { + demo = this; - screen = Screen.startPaintScene(stage); + screen = Screen.startPaintScene(stage); - printer = screen.createPrinter(); - printer.setInk(Color.BLACK); - printer.setFont(textFont); - screen.setKeyOverride((KeyEvent event) -> { - KeyCode code = event.getCode(); - // System.out.println(event); - if (event.isControlDown() || event.isShortcutDown()) { - if (code == KeyCode.Q) { - System.exit(0); - } else if (code == KeyCode.P) { - paused = !paused; - return true; - } else if (code == KeyCode.R) { - printer.cycleMode(true); - drawBackgroundGrid(); - return true; - } else if (code == KeyCode.S) { - if (event.isAltDown()) - screen.fitScaling(); - else - screen.zoomCycle(); - drawBackgroundGrid(); - return true; - } else if (code == KeyCode.A) { - screen.cycleAspect(); - return true; - } else if (code == KeyCode.G) { - grid = !grid; - drawBackgroundGrid(); - return true; - } else if (code == KeyCode.H) { - printHelp(); - printInfo(); - return true; - } else if (code == KeyCode.F) { - stage.setFullScreen(!stage.isFullScreen()); - return true; - } else if (code == KeyCode.M) { - printer.print("\r"); - return true; - } else if (code == KeyCode.L) { - printer.redrawTextPage(); - return true; - } else if (code == KeyCode.DIGIT1) { - DemoPages.printBoxDrawing(printer); - return true; - } else if (code == KeyCode.DIGIT2) { - DemoPages.printZX(printer); - return true; - } else if (code == KeyCode.DIGIT3) { - DemoPages.printBlockPlotting(printer); - return true; - } else if (code == KeyCode.DIGIT4) { - DemoPages.printVideoAttributes(printer); - return true; - } else if (code == KeyCode.DIGIT5) { - DemoPages.printAnsiArt(printer); - return true; - } else if (code == KeyCode.PLUS) { - textFont = textFont.adjust(adjustAmount, 0.0, 0.0, 0.0, 0.0); - printer.setFont(textFont); - printer.redrawTextPage(); - printInfo(); - return true; - } else if (code == KeyCode.MINUS) { - textFont = textFont.adjust(-adjustAmount, 0.0, 0.0, 0.0, 0.0); - printer.setFont(textFont); - printer.redrawTextPage(); - printInfo(); - return true; - } else if (code == KeyCode.LEFT || code == KeyCode.RIGHT || code == KeyCode.UP - || code == KeyCode.DOWN) { - textFont = textFont.adjust(0.0, 0.0, 0.0, adjustX(code), adjustY(code)); - printer.setFont(textFont); - printer.redrawTextPage(); - printInfo(); - return true; - } - } else if (code == KeyCode.LEFT || code == KeyCode.RIGHT || code == KeyCode.UP || code == KeyCode.DOWN) { - textFont = textFont.adjust(0.0, adjustX(code), adjustY(code), 0.0, 0.0); - printer.setFont(textFont); - printer.redrawTextPage(); - printInfo(); - return true; - } else if (code == KeyCode.ENTER) { + printer = screen.createPrinter(); + printer.setInk(Color.BLACK); + printer.setFont(textFont); + screen.setKeyOverride((KeyEvent event) -> { + KeyCode code = event.getCode(); + // System.out.println(event); + if (event.isControlDown() || event.isShortcutDown()) { + if (code == KeyCode.Q) { + System.exit(0); + } else if (code == KeyCode.P) { + paused = !paused; + return true; + } else if (code == KeyCode.R) { + printer.cycleMode(true); + drawBackgroundGrid(); + return true; + } else if (code == KeyCode.S) { + if (event.isAltDown()) { + screen.fitScaling(); + } else { + screen.zoomCycle(); + } + drawBackgroundGrid(); + return true; + } else if (code == KeyCode.A) { + screen.cycleAspect(); + return true; + } else if (code == KeyCode.G) { + grid = !grid; + drawBackgroundGrid(); + return true; + } else if (code == KeyCode.H) { + printHelp(); + printInfo(); + return true; + } else if (code == KeyCode.F) { + stage.setFullScreen(!stage.isFullScreen()); + return true; + } else if (code == KeyCode.M) { + printer.print("\r"); + return true; + } else if (code == KeyCode.L) { + printer.redrawTextPage(); + return true; + } else if (code == KeyCode.DIGIT1) { + DemoPages.printBoxDrawing(printer); + return true; + } else if (code == KeyCode.DIGIT2) { + DemoPages.printZX(printer); + return true; + } else if (code == KeyCode.DIGIT3) { + DemoPages.printBlockPlotting(printer); + return true; + } else if (code == KeyCode.DIGIT4) { + DemoPages.printVideoAttributes(printer); + return true; + } else if (code == KeyCode.DIGIT5) { + DemoPages.printAnsiArt(printer); + return true; + } else if (code == KeyCode.PLUS) { + textFont = textFont.adjust(adjustAmount, 0.0, 0.0, 0.0, 0.0); + printer.setFont(textFont); + printer.redrawTextPage(); + printInfo(); + return true; + } else if (code == KeyCode.MINUS) { + textFont = textFont.adjust(-adjustAmount, 0.0, 0.0, 0.0, 0.0); + printer.setFont(textFont); + printer.redrawTextPage(); + printInfo(); + return true; + } else if (code == KeyCode.LEFT || code == KeyCode.RIGHT || code == KeyCode.UP + || code == KeyCode.DOWN) { + textFont = textFont.adjust(0.0, 0.0, 0.0, adjustX(code), adjustY(code)); + printer.setFont(textFont); + printer.redrawTextPage(); + printInfo(); + return true; + } + } else if (code == KeyCode.LEFT || code == KeyCode.RIGHT || code == KeyCode.UP || code == KeyCode.DOWN) { + textFont = textFont.adjust(0.0, adjustX(code), adjustY(code), 0.0, 0.0); + printer.setFont(textFont); + printer.redrawTextPage(); + printInfo(); + return true; + } else if (code == KeyCode.ENTER) { - printer.print("\n"); - return true; - } - return false; - }); - screen.setKeyTypedHandler((KeyEvent event) -> { - if (!Objects.equals(event.getCharacter(), KeyEvent.CHAR_UNDEFINED)) { - printer.print(event.getCharacter()); - return true; - } - return false; - }); - setup(); + printer.print("\n"); + return true; + } + return false; + }); + screen.setKeyTypedHandler((KeyEvent event) -> { + if (!Objects.equals(event.getCharacter(), KeyEvent.CHAR_UNDEFINED)) { + printer.print(event.getCharacter()); + return true; + } + return false; + }); + setup(); - stage.show(); + stage.show(); - } + } } diff --git a/src/main/java/inf101/v18/gfx/textmode/TextMode.java b/src/main/java/inf101/v18/gfx/textmode/TextMode.java index c3aecde..684e685 100644 --- a/src/main/java/inf101/v18/gfx/textmode/TextMode.java +++ b/src/main/java/inf101/v18/gfx/textmode/TextMode.java @@ -3,156 +3,169 @@ package inf101.v18.gfx.textmode; import inf101.v18.gfx.Screen; public enum TextMode { - /** Low resolution, wide screen (20:11, fits 16:9) text mode 40x22 */ - MODE_40X22(Constants.H40, Constants.V22, Screen.ASPECT_WIDE), - /** Low resolution, 16:10 aspect text mode 40x25 */ - MODE_40X25(Constants.H40, Constants.V25, Screen.ASPECT_MEDIUM), - /** Low resolution, 4:3 aspect text mode 40x30 */ - MODE_40X30(Constants.H40, Constants.V30, Screen.ASPECT_CLASSIC), - /** High resolution, wide screen (20:11, fits 16:9) text mode 80x22 */ - MODE_80X22(Constants.H80, Constants.V22, Screen.ASPECT_WIDE), - /** High resolution, 16:10 aspect text mode 80x25 */ - MODE_80X25(Constants.H80, Constants.V25, Screen.ASPECT_MEDIUM), - /** High resolution, 4:3 aspect text mode 80x30 */ - MODE_80X30(Constants.H80, Constants.V30, Screen.ASPECT_CLASSIC); + /** + * Low resolution, wide screen (20:11, fits 16:9) text mode 40x22 + */ + MODE_40X22(Constants.H40, Constants.V22, Screen.ASPECT_WIDE), + /** + * Low resolution, 16:10 aspect text mode 40x25 + */ + MODE_40X25(Constants.H40, Constants.V25, Screen.ASPECT_MEDIUM), + /** + * Low resolution, 4:3 aspect text mode 40x30 + */ + MODE_40X30(Constants.H40, Constants.V30, Screen.ASPECT_CLASSIC), + /** + * High resolution, wide screen (20:11, fits 16:9) text mode 80x22 + */ + MODE_80X22(Constants.H80, Constants.V22, Screen.ASPECT_WIDE), + /** + * High resolution, 16:10 aspect text mode 80x25 + */ + MODE_80X25(Constants.H80, Constants.V25, Screen.ASPECT_MEDIUM), + /** + * High resolution, 4:3 aspect text mode 80x30 + */ + MODE_80X30(Constants.H80, Constants.V30, Screen.ASPECT_CLASSIC); - protected static class Constants { - protected static final int H40 = 0, H80 = 1; - protected static final int[] HREZ = { 40, 80 }; - protected static final int V22 = 0, V25 = 1, V30 = 2; - protected static final int[] VREZ = { 22, 25, 30 }; - private static TextMode[] MODES = null; + protected static class Constants { + protected static final int H40 = 0, H80 = 1; + protected static final int[] HREZ = {40, 80}; + protected static final int V22 = 0, V25 = 1, V30 = 2; + protected static final int[] VREZ = {22, 25, 30}; + private static TextMode[] MODES = null; - // work around initialization order for statics and enums - protected static TextMode getMode(int i) { - if (MODES == null) - MODES = new TextMode[] { MODE_40X22, MODE_40X25, MODE_40X30, MODE_80X22, MODE_80X25, MODE_80X30 }; - return MODES[(i + MODES.length) % MODES.length]; - } - } + // work around initialization order for statics and enums + protected static TextMode getMode(int i) { + if (MODES == null) { + MODES = new TextMode[]{MODE_40X22, MODE_40X25, MODE_40X30, MODE_80X22, MODE_80X25, MODE_80X30}; + } + return MODES[(i + MODES.length) % MODES.length]; + } + } - /** - * Size of the square-shaped "box" bounds of character cells. - * - * For "high" resolution, characters will be squeezed horizontally to fit half - * the width. - */ - public static final double CHAR_BOX_SIZE = 32; - /** - * Maximum length of a line in any resolution mode - */ - public static final int LINE_WIDTH_MAX = Constants.HREZ[Constants.HREZ.length - 1]; - /** - * Maximum height of a page in any resolution mode - */ - public static final int PAGE_HEIGHT_MAX = Constants.VREZ[Constants.VREZ.length - 1]; - private int aspect; - private int w; + /** + * Size of the square-shaped "box" bounds of character cells. + *

    + * For "high" resolution, characters will be squeezed horizontally to fit half + * the width. + */ + public static final double CHAR_BOX_SIZE = 32; + /** + * Maximum length of a line in any resolution mode + */ + public static final int LINE_WIDTH_MAX = Constants.HREZ[Constants.HREZ.length - 1]; + /** + * Maximum height of a page in any resolution mode + */ + public static final int PAGE_HEIGHT_MAX = Constants.VREZ[Constants.VREZ.length - 1]; + private int aspect; + private int w; - private int h; + private int h; - private int hIndex; + private int hIndex; - private int vIndex; + private int vIndex; - TextMode(int w, int h, int aspect) { - this.hIndex = w; - this.vIndex = h; - this.aspect = aspect; - this.w = Constants.HREZ[w]; - this.h = Constants.VREZ[h]; - } + TextMode(int w, int h, int aspect) { + this.hIndex = w; + this.vIndex = h; + this.aspect = aspect; + this.w = Constants.HREZ[w]; + this.h = Constants.VREZ[h]; + } - private TextMode findMode(int hIndex, int vIndex) { - hIndex = (hIndex + Constants.HREZ.length) % Constants.HREZ.length; - vIndex = (vIndex + Constants.VREZ.length) % Constants.VREZ.length; - return Constants.getMode(vIndex + hIndex * Constants.VREZ.length); - } + private TextMode findMode(int hIndex, int vIndex) { + hIndex = (hIndex + Constants.HREZ.length) % Constants.HREZ.length; + vIndex = (vIndex + Constants.VREZ.length) % Constants.VREZ.length; + return Constants.getMode(vIndex + hIndex * Constants.VREZ.length); + } - /** - * Get aspect ration descriptor for use with {@link Screen#setAspect()} - * - * @return One of {@link Screen#ASPECT_WIDE}, {@link Screen#ASPECT_MEDIUM} or - * {@link Screen#ASPECT_CLASSIC} - */ - public int getAspect() { - return aspect; - } + /** + * Get aspect ration descriptor for use with {@link Screen#setAspect()} + * + * @return One of {@link Screen#ASPECT_WIDE}, {@link Screen#ASPECT_MEDIUM} or + * {@link Screen#ASPECT_CLASSIC} + */ + public int getAspect() { + return aspect; + } - public double getCharBoxSize() { - return CHAR_BOX_SIZE; - } + public double getCharBoxSize() { + return CHAR_BOX_SIZE; + } - public double getCharHeight() { - return CHAR_BOX_SIZE; - } + public double getCharHeight() { + return CHAR_BOX_SIZE; + } - public double getCharWidth() { - return w == 80 ? CHAR_BOX_SIZE / 2 : CHAR_BOX_SIZE; - } + public double getCharWidth() { + return w == 80 ? CHAR_BOX_SIZE / 2 : CHAR_BOX_SIZE; + } - public int getLineWidth() { - return w; - } + public int getLineWidth() { + return w; + } - public int getPageHeight() { - return h; - } + public int getPageHeight() { + return h; + } - /** - * Cycle through horizontal modes - * - * @return Next available horizontal mode (vertical resolution unchanged) - */ - public TextMode nextHorizMode() { - return findMode((hIndex + 1) % Constants.HREZ.length, vIndex); - } + /** + * Cycle through horizontal modes + * + * @return Next available horizontal mode (vertical resolution unchanged) + */ + public TextMode nextHorizMode() { + return findMode((hIndex + 1) % Constants.HREZ.length, vIndex); + } - /** - * Cycle through modes - * - * @return Next available mode - */ - public TextMode nextMode() { - int m = vIndex + hIndex * Constants.VREZ.length; - return Constants.getMode(m + 1); - } + /** + * Cycle through modes + * + * @return Next available mode + */ + public TextMode nextMode() { + int m = vIndex + hIndex * Constants.VREZ.length; + return Constants.getMode(m + 1); + } - /** - * Cycle through vertical modes - * - * @return Next available vertical mode (horizontal resolution unchanged) - */ - public TextMode nextVertMode() { - return findMode((hIndex + 1) % Constants.HREZ.length, vIndex); - } + /** + * Cycle through vertical modes + * + * @return Next available vertical mode (horizontal resolution unchanged) + */ + public TextMode nextVertMode() { + return findMode((hIndex + 1) % Constants.HREZ.length, vIndex); + } - /** - * Cycle through horizontal modes - * - * @return Previous available horizontal mode (vertical resolution unchanged) - */ - public TextMode prevHorizMode() { - return findMode((hIndex - 1) % Constants.HREZ.length, vIndex); - } + /** + * Cycle through horizontal modes + * + * @return Previous available horizontal mode (vertical resolution unchanged) + */ + public TextMode prevHorizMode() { + return findMode((hIndex - 1) % Constants.HREZ.length, vIndex); + } - /** - * Cycle through modes - * - * @return Previous available mode - */ - public TextMode prevMode() { - int m = vIndex + hIndex * Constants.VREZ.length; - return Constants.getMode(m); - } + /** + * Cycle through modes + * + * @return Previous available mode + */ + public TextMode prevMode() { + int m = vIndex + hIndex * Constants.VREZ.length; + return Constants.getMode(m); + } - /** - * Cycle through vertical modes - * - * @return Previous available vertical mode (horizontal resolution unchanged) - */ - public TextMode prevVertMode() { - return findMode((hIndex - 1) % Constants.HREZ.length, vIndex); - } + /** + * Cycle through vertical modes + * + * @return Previous available vertical mode (horizontal resolution unchanged) + */ + public TextMode prevVertMode() { + return findMode((hIndex - 1) % Constants.HREZ.length, vIndex); + } } diff --git a/src/main/java/inf101/v18/grid/GridDirection.java b/src/main/java/inf101/v18/grid/GridDirection.java index cae813b..a2c878b 100644 --- a/src/main/java/inf101/v18/grid/GridDirection.java +++ b/src/main/java/inf101/v18/grid/GridDirection.java @@ -4,66 +4,73 @@ import java.util.Arrays; import java.util.List; public enum GridDirection { - EAST(0, 1, 0, 4), NORTH(90, 0, -1, 1), WEST(180, -1, 0, 8), SOUTH(270, 0, 1, 2), // - NORTHEAST(45, 1, -1, 5), NORTHWEST(135, -1, -1, 9), SOUTHWEST(225, -1, 1, 10), SOUTHEAST(315, 1, 1, 6), // - CENTER(0, 0, 0, 0); - /** - * The four cardinal directions: {@link #NORTH}, {@link #SOUTH}, {@link #EAST}, - * {@link #WEST}. - */ - public static final List FOUR_DIRECTIONS = Arrays.asList(EAST, NORTH, WEST, SOUTH); - /** - * The eight cardinal and intercardinal directions: {@link #NORTH}, - * {@link #SOUTH}, {@link #EAST}, {@link #WEST}, {@link #NORTHWEST}, - * {@link #NORTHEAST}, {@link #SOUTHWEST}, {@link #SOUTHEAST}. - */ - public static final List EIGHT_DIRECTIONS = Arrays.asList(EAST, NORTHEAST, NORTH, NORTHWEST, WEST, - SOUTHWEST, SOUTH, SOUTHEAST); - /** - * The eight cardinal and intercardinal directions ({@link #EIGHT_DIRECTIONS}), - * plus {@link #CENTER}. - */ - public static final List NINE_DIRECTIONS = Arrays.asList(EAST, NORTHEAST, NORTH, NORTHWEST, WEST, - SOUTHWEST, SOUTH, SOUTHEAST, CENTER); + EAST(0, 1, 0, 4), + NORTH(90, 0, -1, 1), + WEST(180, -1, 0, 8), + SOUTH(270, 0, 1, 2), + NORTHEAST(45, 1, -1, 5), + NORTHWEST(135, -1, -1, 9), + SOUTHWEST(225, -1, 1, 10), + SOUTHEAST(315, 1, 1, 6), + CENTER(0, 0, 0, 0); - private final double degrees; - private final int dx; - private final int dy; - private final int mask; + /** + * The four cardinal directions: {@link #NORTH}, {@link #SOUTH}, {@link #EAST}, + * {@link #WEST}. + */ + public static final List FOUR_DIRECTIONS = Arrays.asList(EAST, NORTH, WEST, SOUTH); + /** + * The eight cardinal and intercardinal directions: {@link #NORTH}, + * {@link #SOUTH}, {@link #EAST}, {@link #WEST}, {@link #NORTHWEST}, + * {@link #NORTHEAST}, {@link #SOUTHWEST}, {@link #SOUTHEAST}. + */ + public static final List EIGHT_DIRECTIONS = Arrays.asList(EAST, NORTHEAST, NORTH, NORTHWEST, WEST, + SOUTHWEST, SOUTH, SOUTHEAST); + /** + * The eight cardinal and intercardinal directions ({@link #EIGHT_DIRECTIONS}), + * plus {@link #CENTER}. + */ + public static final List NINE_DIRECTIONS = Arrays.asList(EAST, NORTHEAST, NORTH, NORTHWEST, WEST, + SOUTHWEST, SOUTH, SOUTHEAST, CENTER); - GridDirection(double degrees, int dx, int dy, int mask) { - this.degrees = degrees; - this.dx = dx; - this.dy = dy; - this.mask = mask; - } + private final double degrees; + private final int dx; + private final int dy; + private final int mask; - /** - * @return The angle of this direction, with 0° facing due {@link #EAST} and 90° - * being {@link #NORTH}. - */ - public double getDegrees() { - return degrees; - } + GridDirection(double degrees, int dx, int dy, int mask) { + this.degrees = degrees; + this.dx = dx; + this.dy = dy; + this.mask = mask; + } - /** - * @return The change to your X-coordinate if you were to move one step in this - * direction - */ - public int getDx() { - return dx; - } + /** + * @return The angle of this direction, with 0° facing due {@link #EAST} and 90° + * being {@link #NORTH}. + */ + public double getDegrees() { + return degrees; + } - /** - * @return The change to your Y-coordinate if you were to move one step in this - * direction - */ - public int getDy() { - return dy; - } + /** + * @return The change to your X-coordinate if you were to move one step in this + * direction + */ + public int getDx() { + return dx; + } - public int getMask() { - return mask; - } + /** + * @return The change to your Y-coordinate if you were to move one step in this + * direction + */ + public int getDy() { + return dy; + } + + public int getMask() { + return mask; + } } diff --git a/src/main/java/inf101/v18/grid/IArea.java b/src/main/java/inf101/v18/grid/IArea.java index 7033ef9..122ba07 100644 --- a/src/main/java/inf101/v18/grid/IArea.java +++ b/src/main/java/inf101/v18/grid/IArea.java @@ -10,152 +10,150 @@ import java.util.stream.Stream; * locations within the area. *

    * See {@link #location(int, int)} to covert (x,y)-coordinates to - * {@link ILocation}s. + * {@link Location}s. *

    * An {@link IArea} does not store anything, it just defines valid * storage locations (e.g., for an {@link IGrid}), and the relationships between * locations (e.g., how to find neighbours of a given location). - * + * * @author Anya Helene Bagge, UiB */ -public interface IArea extends Iterable { - /** - * Check if a (x,y) is inside the area. - * - * @param x - * X-coordinate - * @param y - * Y-coordinate - * @return True if the (x,y) position lies within the area - */ - boolean contains(int x, int y); +public interface IArea extends Iterable { + /** + * Check if a (x,y) is inside the area. + * + * @param x X-coordinate + * @param y Y-coordinate + * @return True if the (x,y) position lies within the area + */ + boolean contains(int x, int y); - /** - * Check if a position is inside the area. - * - * @param pos - * A position - * @return True if the position lies within the area - */ - boolean contains(IPosition pos); + /** + * Check if a position is inside the area. + * + * @param pos A position + * @return True if the position lies within the area + */ + boolean contains(IPosition pos); - @Override - boolean equals(Object other); + @Override + boolean equals(Object other); - /** - * Convert a 1D coordinate to a location - *

    - * Returns a location l = fromIndex(i) such that - * toIndex(l.getX(), l.getY()) == i. - * - * @param i - * @return A location - */ - ILocation fromIndex(int i); + /** + * Convert a 1D coordinate to a location + *

    + * Returns a location l = fromIndex(i) such that + * toIndex(l.getX(), l.getY()) == i. + * + * @param i + * @return A location + */ + Location fromIndex(int i); - /** @return Height of the area */ - int getHeight(); + /** + * @return Height of the area + */ + int getHeight(); - /** - * Returns the number of legal positions in the area - * - * @return Same as getWidth()*getHeight() - */ - int getSize(); + /** + * Returns the number of legal positions in the area + * + * @return Same as getWidth()*getHeight() + */ + int getSize(); - /** @return Width of the area */ - int getWidth(); + /** + * @return Width of the area + */ + int getWidth(); - @Override - int hashCode(); + @Override + int hashCode(); - /** - * Get a location object corresponding to (x,y) - * - * @param x - * X-coordinate - * @param y - * Y-coordinate - * @return The location object associated with (x,y) - * @throws IndexOutOfBoundsException - * if {@link #contains(int, int)} returns false for (x,y) - */ - ILocation location(int x, int y); + /** + * Get a location object corresponding to (x,y) + * + * @param x X-coordinate + * @param y Y-coordinate + * @return The location object associated with (x,y) + * @throws IndexOutOfBoundsException if {@link #contains(int, int)} returns false for (x,y) + */ + Location location(int x, int y); - /** - * Get all locations in area - *

    - * Since IArea is @{@link Iterable}, you can also use directly in a for-loop to - * iterate over the locations. - *

    - * All locations in the list are guaranteed to be valid according to - * {@link #isValid(ILocation)}. The returned list cannot be modified. - * - * @return An unmodifiable list with all the locations in the area - */ - List locations(); + /** + * Get all locations in area + *

    + * Since IArea is @{@link Iterable}, you can also use directly in a for-loop to + * iterate over the locations. + *

    + * All locations in the list are guaranteed to be valid according to + * {@link #isValid(Location)}. The returned list cannot be modified. + * + * @return An unmodifiable list with all the locations in the area + */ + List locations(); - /** - * Return an object for iterating over all the neighbours of the given position, - * suitable for use in a new-style for-loop. - *

    - * The iterator will yield up to eight positions (less if the given position is - * at the edge of the area, and the coordinates are not wrapped). E.g., for a - * 1x1 area, the iterator will yield nothing (if the area is not wrapped), or - * the same position two or eight times (if the area is wrapped horizontally, - * vertically or both). - * - * @param pos - * A position in the area - * @return An iterable over positions, with {@link #contains(ILocation)} being - * true for each position. - * @see #wrapsHorizontally(), {@link #wrapsVertically()} - * @throws IndexOutOfBoundsException - * if !contains(pos) - */ - Iterable neighboursOf(ILocation pos); + /** + * Return an object for iterating over all the neighbours of the given position, + * suitable for use in a new-style for-loop. + *

    + * The iterator will yield up to eight positions (less if the given position is + * at the edge of the area, and the coordinates are not wrapped). E.g., for a + * 1x1 area, the iterator will yield nothing (if the area is not wrapped), or + * the same position two or eight times (if the area is wrapped horizontally, + * vertically or both). + * + * @param pos A position in the area + * @return An iterable over positions, with {@link #contains(Location)} being + * true for each position. + * @throws IndexOutOfBoundsException if !contains(pos) + * @see #wrapsHorizontally(), {@link #wrapsVertically()} + */ + Iterable neighboursOf(Location pos); - /** @return A (possibly) parallel {@link Stream} of all locations in the area */ - Stream parallelStream(); + /** + * @return A (possibly) parallel {@link Stream} of all locations in the area + */ + Stream parallelStream(); - /** @return A {@link Stream} of all locations in the area */ - Stream stream(); + /** + * @return A {@link Stream} of all locations in the area + */ + Stream stream(); - /** - * Convert a 2D coordinate to 1D - * - * @param x - * X-coordinate - * @param y - * Y-coordinate - * @return x + y*getWidth() - */ - int toIndex(int x, int y); + /** + * Convert a 2D coordinate to 1D + * + * @param x X-coordinate + * @param y Y-coordinate + * @return x + y*getWidth() + */ + int toIndex(int x, int y); - @Override - String toString(); + @Override + String toString(); - /** - * If the area wraps horizontally, then x will be the same as x+(k*getWidth()) - * for any k – i.e., it will be as if the 2D area is projected on a cylinder or - * torus in 3D-space. - *

    - * With no wrapping, accessing positions outside (0,0)–(getWidth(),getHeight()) - * is illegal. - * - * @return True if the area wraps around horizontally - */ - boolean wrapsHorizontally(); + /** + * If the area wraps horizontally, then x will be the same as x+(k*getWidth()) + * for any k – i.e., it will be as if the 2D area is projected on a cylinder or + * torus in 3D-space. + *

    + * With no wrapping, accessing positions outside (0,0)–(getWidth(),getHeight()) + * is illegal. + * + * @return True if the area wraps around horizontally + */ + boolean wrapsHorizontally(); - /** - * If the area wraps vertically, then y will be the same as y+(k*getHeight()) - * for any k – i.e., it will be as if the 2D area is projected on a cylinder or - * torus in 3D-space. - *

    - * With no wrapping, accessing positions outside (0,0)–(getWidth(),getHeight()) - * is illegal. - * - * @return True if the area wraps around vertically - */ - boolean wrapsVertically(); + /** + * If the area wraps vertically, then y will be the same as y+(k*getHeight()) + * for any k – i.e., it will be as if the 2D area is projected on a cylinder or + * torus in 3D-space. + *

    + * With no wrapping, accessing positions outside (0,0)–(getWidth(),getHeight()) + * is illegal. + * + * @return True if the area wraps around vertically + */ + boolean wrapsVertically(); } diff --git a/src/main/java/inf101/v18/grid/IGrid.java b/src/main/java/inf101/v18/grid/IGrid.java index 2f1a415..c343621 100644 --- a/src/main/java/inf101/v18/grid/IGrid.java +++ b/src/main/java/inf101/v18/grid/IGrid.java @@ -5,209 +5,194 @@ import java.util.stream.Stream; public interface IGrid extends Iterable { - /** - * Make a copy - * - * @return A fresh copy of the grid, with the same elements - */ - IGrid copy(); + /** + * Make a copy + * + * @return A fresh copy of the grid, with the same elements + */ + IGrid copy(); - /** - * Create a parallel {@link Stream} with all the elements in this grid. - * - * @return A stream - * @see {@link java.util.Collection#parallelStream()} - */ - Stream elementParallelStream(); + /** + * Create a parallel {@link Stream} with all the elements in this grid. + * + * @return A stream + * @see {@link java.util.Collection#parallelStream()} + */ + Stream elementParallelStream(); - /** - * Create a {@link Stream} with all the elements in this grid. - * - * @return A stream - */ - Stream elementStream(); + /** + * Create a {@link Stream} with all the elements in this grid. + * + * @return A stream + */ + Stream elementStream(); - /** - * Initialise the contents of all cells using an initialiser function. - * - * The function will be called with the (x,y) position of an element, and is - * expected to return the element to place at that position. For example: - * - *

    -	 * // fill all cells with the position as a string (e.g., "(2,2)")
    -	 * grid.setAll((x, y) -> String.format("(%d,%d)", x, y));
    -	 * 
    - * - * @param initialiser - * The initialiser function - */ - void fill(Function initialiser); + /** + * Initialise the contents of all cells using an initialiser function. + *

    + * The function will be called with the (x,y) position of an element, and is + * expected to return the element to place at that position. For example: + * + *

    +     * // fill all cells with the position as a string (e.g., "(2,2)")
    +     * grid.setAll((x, y) -> String.format("(%d,%d)", x, y));
    +     * 
    + * + * @param initialiser The initialiser function + */ + void fill(Function initialiser); - /** - * Set the contents of all cells to element - * - * For example: - * - *
    -	 * // clear the grid
    -	 * grid.setAll(null);
    -	 * 
    - * - * @param element - */ - void fill(T element); + /** + * Set the contents of all cells to element + *

    + * For example: + * + *

    +     * // clear the grid
    +     * grid.setAll(null);
    +     * 
    + * + * @param element + */ + void fill(T element); - /** - * Get the contents of the cell in the given x,y location. - * - * y must be greater than or equal to 0 and less than getHeight(). x must be - * greater than or equal to 0 and less than getWidth(). - * - * @param pos - * The (x,y) position of the grid cell to get the contents of. - * @throws IndexOutOfBoundsException - * if !isValid(pos) - */ - T get(ILocation pos); + /** + * Get the contents of the cell in the given x,y location. + *

    + * y must be greater than or equal to 0 and less than getHeight(). x must be + * greater than or equal to 0 and less than getWidth(). + * + * @param pos The (x,y) position of the grid cell to get the contents of. + * @throws IndexOutOfBoundsException if !isValid(pos) + */ + T get(Location pos); - /** - * Get the contents of the cell in the given x,y location. - * - * y must be greater than or equal to 0 and less than getHeight(). x must be - * greater than or equal to 0 and less than getWidth(). - * - * @param x - * The column of the cell to get the contents of. - * @param y - * The row of the cell to get contents of. - * @throws IndexOutOfBoundsException - * if !isValid(x,y) - */ - T get(int x, int y); + /** + * Get the contents of the cell in the given x,y location. + *

    + * y must be greater than or equal to 0 and less than getHeight(). x must be + * greater than or equal to 0 and less than getWidth(). + * + * @param x The column of the cell to get the contents of. + * @param y The row of the cell to get contents of. + * @throws IndexOutOfBoundsException if !isValid(x,y) + */ + T get(int x, int y); - IArea getArea(); + IArea getArea(); - /** @return The height of the grid. */ - int getHeight(); + /** + * @return The height of the grid. + */ + int getHeight(); - /** - * Get the contents of the cell in the given x,y location. - * - * y must be greater than or equal to 0 and less than getHeight(). x must be - * greater than or equal to 0 and less than getWidth(). - * - * @param pos - * The (x,y) position of the grid cell to get the contents of. - * @param defaultResult - * A default value to be substituted if the (x,y) is out of bounds or - * contents == null. - */ - T getOrDefault(ILocation pos, T defaultResult); + /** + * Get the contents of the cell in the given x,y location. + *

    + * y must be greater than or equal to 0 and less than getHeight(). x must be + * greater than or equal to 0 and less than getWidth(). + * + * @param pos The (x,y) position of the grid cell to get the contents of. + * @param defaultResult A default value to be substituted if the (x,y) is out of bounds or + * contents == null. + */ + T getOrDefault(Location pos, T defaultResult); - /** - * Get the contents of the cell in the given x,y location. - * - * y must be greater than or equal to 0 and less than getHeight(). x must be - * greater than or equal to 0 and less than getWidth(). - * - * @param x - * The column of the cell to get the contents of. - * @param y - * The row of the cell to get contents of. - * @param defaultResult - * A default value to be substituted if the (x,y) is out of bounds or - * contents == null. - */ - T getOrDefault(int x, int y, T defaultResult); + /** + * Get the contents of the cell in the given x,y location. + *

    + * y must be greater than or equal to 0 and less than getHeight(). x must be + * greater than or equal to 0 and less than getWidth(). + * + * @param x The column of the cell to get the contents of. + * @param y The row of the cell to get contents of. + * @param defaultResult A default value to be substituted if the (x,y) is out of bounds or + * contents == null. + */ + T getOrDefault(int x, int y, T defaultResult); - /** @return The width of the grid. */ - int getWidth(); + /** + * @return The width of the grid. + */ + int getWidth(); - /** - * Check if coordinates are valid. - * - * Valid coordinates are 0 <= pos.getX() < getWidth(), 0 <= pos.getY() < - * getHeight(). - * - * @param pos - * A position - * @return true if the position is within the grid - */ - boolean isValid(ILocation pos); + /** + * Check if coordinates are valid. + *

    + * Valid coordinates are 0 <= pos.getX() < getWidth(), 0 <= pos.getY() < + * getHeight(). + * + * @param pos A position + * @return true if the position is within the grid + */ + boolean isValid(Location pos); - /** - * Check if coordinates are valid. - * - * Valid coordinates are 0 <= x < getWidth(), 0 <= y < getHeight(). - * - * @param x - * an x coordinate - * @param y - * an y coordinate - * @return true if the (x,y) position is within the grid - */ - boolean isValid(int x, int y); + /** + * Check if coordinates are valid. + *

    + * Valid coordinates are 0 <= x < getWidth(), 0 <= y < getHeight(). + * + * @param x an x coordinate + * @param y an y coordinate + * @return true if the (x,y) position is within the grid + */ + boolean isValid(int x, int y); - /** - * Create a parallel {@link Stream} with all the locations in this grid. - *

    - * All locations obtained through the stream are guaranteed to be valid - * according to {@link #isValid(ILocation)}. - * - * @return A stream - * @see {@link java.util.Collection#parallelStream()} - */ - Stream locationParallelStream(); + /** + * Create a parallel {@link Stream} with all the locations in this grid. + *

    + * All locations obtained through the stream are guaranteed to be valid + * according to {@link #isValid(Location)}. + * + * @return A stream + * @see {@link java.util.Collection#parallelStream()} + */ + Stream locationParallelStream(); - /** - * Iterate over all grid locations - *

    - * See also {@link #iterator()} – using the grid directly in a for-loop will - * iterate over the elements. - *

    - * All locations obtained through the iterator are guaranteed to be valid - * according to {@link #isValid(ILocation)}. - * - * @return An iterable for iterating over all the locations in the grid - */ - Iterable locations(); + /** + * Iterate over all grid locations + *

    + * See also {@link #iterator()} – using the grid directly in a for-loop will + * iterate over the elements. + *

    + * All locations obtained through the iterator are guaranteed to be valid + * according to {@link #isValid(Location)}. + * + * @return An iterable for iterating over all the locations in the grid + */ + Iterable locations(); - /** - * Create a {@link Stream} with all the locations in this grid. - *

    - * All locations obtained through the stream are guaranteed to be valid - * according to {@link #isValid(ILocation)}. - * - * @return A stream - */ - Stream locationStream(); + /** + * Create a {@link Stream} with all the locations in this grid. + *

    + * All locations obtained through the stream are guaranteed to be valid + * according to {@link #isValid(Location)}. + * + * @return A stream + */ + Stream locationStream(); - /** - * Set the contents of the cell in the given x,y location. - * - * y must be greater than or equal to 0 and less than getHeight(). x must be - * greater than or equal to 0 and less than getWidth(). - * - * @param pos - * The (x,y) position of the grid cell to get the contents of. - * @param element - * The contents the cell is to have. - * @throws IndexOutOfBoundsException - * if !isValid(pos) - */ - void set(ILocation pos, T element); + /** + * Set the contents of the cell in the given x,y location. + *

    + * y must be greater than or equal to 0 and less than getHeight(). x must be + * greater than or equal to 0 and less than getWidth(). + * + * @param pos The (x,y) position of the grid cell to get the contents of. + * @param element The contents the cell is to have. + * @throws IndexOutOfBoundsException if !isValid(pos) + */ + void set(Location pos, T element); - /** - * Set the contents of the cell in the given x,y location. - * - * y must be greater than or equal to 0 and less than getHeight(). x must be - * greater than or equal to 0 and less than getWidth(). - * - * @param element - * The contents the cell is to have. - * @throws IndexOutOfBoundsException - * if !isValid(x,y) - */ - void set(int x, int y, T element); + /** + * Set the contents of the cell in the given x,y location. + *

    + * y must be greater than or equal to 0 and less than getHeight(). x must be + * greater than or equal to 0 and less than getWidth(). + * + * @param element The contents the cell is to have. + * @throws IndexOutOfBoundsException if !isValid(x,y) + */ + void set(int x, int y, T element); } \ No newline at end of file diff --git a/src/main/java/inf101/v18/grid/ILocation.java b/src/main/java/inf101/v18/grid/ILocation.java deleted file mode 100644 index c274807..0000000 --- a/src/main/java/inf101/v18/grid/ILocation.java +++ /dev/null @@ -1,107 +0,0 @@ -package inf101.v18.grid; - -import java.util.Collection; -import java.util.List; - -/** - * Represents a location within an {@link IArea}. - *

    - * Immutable: Locations are immutable; i.e., a particular - * ILocation will always have the same x- and y-coordinates and - * cannot be changed. Methods that find neighbours (such as - * {@link #go(GridDirection)}) will return another ILocation. - *

    - * Area: All {@link ILocation}s are tied to an {@link IArea}, thus they - * will know whether they are on the edge of the area (and not all neighbouring - * grid coordinates will be valid locations within the area) and other - * coordinate system properties that are determined by the {@link IArea} (e.g., - * if the coordinate system wraps around like on the surface of a donut). - *

    - * Unique objects: All {@link ILocation} in an {@link IArea} are - * unique. This means that area.location(x,y) == area.location(x,y) - * for all x and y, and that: - * - *

    - * // equality is reference equality for locations in the same area
    - * if (l1.getArea() == l2.getArea())
    - * 	assertEquals(l1.equals(l2), (l1 == l2));
    - * 
    - * - * - * @author Anya Helene Bagge, UiB - */ -public interface ILocation extends IPosition { - - /** - * Iterate over neighbours in eight directions. - *

    - * (The iterator may yield fewer than eight locations if the current location is - * at the edge of its containing area. - * - * @return The neighbours in the eight cardinal and intercardinal directions - * ({@link GridDirection#NORTH}, @link GridDirection#SOUTH}, @link - * GridDirection#EAST}, @link GridDirection#WEST}, - * {@link GridDirection#NORTHEAST}, @link - * GridDirection#NORTHWEST}, @link GridDirection#SOUTHEAST}, @link - * GridDirection#SOUTHWEST}, ) - */ - Collection allNeighbours(); - - /** - * Checks whether you can go towards direction dir without going outside the - * area bounds - * - * @param dir - * @return True if go(dir) will succeed - */ - boolean canGo(GridDirection dir); - - /** - * Iterate over north/south/east/west neighbours. - *

    - * (The iterator may yield fewer than four locations if the current location is - * at the edge of its containing area. - * - * @return The neighbours in the four cardinal directions - * ({@link GridDirection#NORTH}, @link GridDirection#SOUTH}, @link - * GridDirection#EAST}, @link GridDirection#WEST} - */ - Collection cardinalNeighbours(); - - IArea getArea(); - - int getIndex(); - - /** - * Return the next location in direction dir. - *

    - * This does not change the location object; rather it returns the - * ILocation you would end up at if you go in the given direction from this - * ILocation. - * - * @param dir - * A direction - * @return A neighbouring location - * @throws IndexOutOfBoundsException - * if !canGo(dir) - */ - ILocation go(GridDirection dir); - - /** - * Find the grid cells between this location (exclusive) and another location - * (inclusive). - * - * This is a list of length {@link #gridDistanceTo(other)}, containing - * the cells that a chess queen would visit when moving to other. - *

    - * Computes the maximum of the horizontal and the vertical distance. For - * example, to go from (0,0) to (3,5), you could go three steps SOUTHEAST and - * two steps SOUTH, so the {@link #gridDistanceTo(IPosition)} is 5. - * - * @param other - * @return Number of horizontal/vertical/diagonal (queen-like) steps to - * other - */ - List gridLineTo(ILocation other); - -} \ No newline at end of file diff --git a/src/main/java/inf101/v18/grid/IMultiGrid.java b/src/main/java/inf101/v18/grid/IMultiGrid.java index f5c1a5e..86af58e 100644 --- a/src/main/java/inf101/v18/grid/IMultiGrid.java +++ b/src/main/java/inf101/v18/grid/IMultiGrid.java @@ -5,210 +5,172 @@ import java.util.function.Predicate; import java.util.stream.Collectors; public interface IMultiGrid extends IGrid> { - /** - * Add to the cell at the given location. - * - * @param loc - * The (x,y) position of the grid cell to get the contents of. - * @param element - * An element to be added to the cell. - * @throws IndexOutOfBoundsException - * if !isValid(loc) - */ - default void add(ILocation loc, T element) { - get(loc).add(element); - } + /** + * Add to the cell at the given location. + * + * @param loc The (x,y) position of the grid cell to get the contents of. + * @param element An element to be added to the cell. + * @throws IndexOutOfBoundsException if !isValid(loc) + */ + default void add(Location loc, T element) { + get(loc).add(element); + } - /** - * Add to the cell at the given x,y location. - * - * y must be greater than or equal to 0 and less than getHeight(). x must be - * greater than or equal to 0 and less than getWidth(). - * - * @param pos - * The (x,y) position of the grid cell to get the contents of. - * @param element - * An element to be added to the cell - * @throws IndexOutOfBoundsException - * if !isValid(x,y) - */ - default void add(int x, int y, T element) { - get(x, y).add(element); - } + /** + * Add to the cell at the given x,y location. + *

    + * y must be greater than or equal to 0 and less than getHeight(). x must be + * greater than or equal to 0 and less than getWidth(). + * + * @param pos The (x,y) position of the grid cell to get the contents of. + * @param element An element to be added to the cell + * @throws IndexOutOfBoundsException if !isValid(x,y) + */ + default void add(int x, int y, T element) { + get(x, y).add(element); + } - /** - * Check if a cell contains an element. - * - * - * @param loc - * The (x,y) position of the grid cell - * @param predicate - * Search predicate. - * @return true if an element matching the predicate was found - * @throws IndexOutOfBoundsException - * if !isValid(loc) - */ - default boolean contains(ILocation loc, Predicate predicate) { - for (T t : get(loc)) { - if (predicate.test(t)) - return true; - } - return false; - } + /** + * Check if a cell contains an element. + * + * @param loc The (x,y) position of the grid cell + * @param predicate Search predicate. + * @return true if an element matching the predicate was found + * @throws IndexOutOfBoundsException if !isValid(loc) + */ + default boolean contains(Location loc, Predicate predicate) { + for (T t : get(loc)) { + if (predicate.test(t)) { + return true; + } + } + return false; + } - /** - * Check if a cell contains an element. - * - * - * @param loc - * The (x,y) position of the grid cell - * @param element - * An element to search for. - * @return true if element is at the given location - * @throws IndexOutOfBoundsException - * if !isValid(loc) - */ - default boolean contains(ILocation loc, T element) { - return get(loc).contains(element); - } + /** + * Check if a cell contains an element. + * + * @param loc The (x,y) position of the grid cell + * @param element An element to search for. + * @return true if element is at the given location + * @throws IndexOutOfBoundsException if !isValid(loc) + */ + default boolean contains(Location loc, T element) { + return get(loc).contains(element); + } - /** - * Check if a cell contains an element. - * - * y must be greater than or equal to 0 and less than getHeight(). x must be - * greater than or equal to 0 and less than getWidth(). - * - * @param pos - * The (x,y) position of the grid cell to get the contents of. - * @param predicate - * Search predicate. - * @return true if an element matching the predicate was found - * @throws IndexOutOfBoundsException - * if !isValid(x,y) - */ - default boolean contains(int x, int y, Predicate predicate) { - return contains(this.getArea().location(x, y), predicate); - } + /** + * Check if a cell contains an element. + *

    + * y must be greater than or equal to 0 and less than getHeight(). x must be + * greater than or equal to 0 and less than getWidth(). + * + * @param pos The (x,y) position of the grid cell to get the contents of. + * @param predicate Search predicate. + * @return true if an element matching the predicate was found + * @throws IndexOutOfBoundsException if !isValid(x,y) + */ + default boolean contains(int x, int y, Predicate predicate) { + return contains(this.getArea().location(x, y), predicate); + } - /** - * Check if a cell contains an element. - * - * y must be greater than or equal to 0 and less than getHeight(). x must be - * greater than or equal to 0 and less than getWidth(). - * - * @param pos - * The (x,y) position of the grid cell to get the contents of. - * @param element - * An element to search for. - * @return true if element is at the given location - * @throws IndexOutOfBoundsException - * if !isValid(x,y) - */ - default boolean contains(int x, int y, T element) { - return get(x, y).contains(element); - } + /** + * Check if a cell contains an element. + *

    + * y must be greater than or equal to 0 and less than getHeight(). x must be + * greater than or equal to 0 and less than getWidth(). + * + * @param pos The (x,y) position of the grid cell to get the contents of. + * @param element An element to search for. + * @return true if element is at the given location + * @throws IndexOutOfBoundsException if !isValid(x,y) + */ + default boolean contains(int x, int y, T element) { + return get(x, y).contains(element); + } - /** - * Get all elements in a cell that match the predicate - * - * - * @param loc - * The (x,y) position of the grid cell - * @param predicate - * Search predicate. - * @return true if an element matching the predicate was found - * @throws IndexOutOfBoundsException - * if !isValid(loc) - */ - default List get(ILocation loc, Predicate predicate) { - return get(loc).stream().filter(predicate).collect(Collectors.toList()); - } + /** + * Get all elements in a cell that match the predicate + * + * @param loc The (x,y) position of the grid cell + * @param predicate Search predicate. + * @return true if an element matching the predicate was found + * @throws IndexOutOfBoundsException if !isValid(loc) + */ + default List get(Location loc, Predicate predicate) { + return get(loc).stream().filter(predicate).collect(Collectors.toList()); + } - /** - * Check if a cell contains an element. - * - * y must be greater than or equal to 0 and less than getHeight(). x must be - * greater than or equal to 0 and less than getWidth(). - * - * @param pos - * The (x,y) position of the grid cell to get the contents of. - * @param predicate - * Search predicate. - * @return true if an element matching the predicate was found - * @throws IndexOutOfBoundsException - * if !isValid(x,y) - */ - default List get(int x, int y, Predicate predicate) { - return get(this.getArea().location(x, y), predicate); - } + /** + * Check if a cell contains an element. + *

    + * y must be greater than or equal to 0 and less than getHeight(). x must be + * greater than or equal to 0 and less than getWidth(). + * + * @param pos The (x,y) position of the grid cell to get the contents of. + * @param predicate Search predicate. + * @return true if an element matching the predicate was found + * @throws IndexOutOfBoundsException if !isValid(x,y) + */ + default List get(int x, int y, Predicate predicate) { + return get(this.getArea().location(x, y), predicate); + } - /** - * Remove an element from the cell at the given location. - * - * @param loc - * The location of the grid cell - * @param predicate - * Predicate which should be true for elements to be removed - * @return Number of elements removed - * @throws IndexOutOfBoundsException - * if !isValid(loc) - */ - default int remove(ILocation loc, Predicate predicate) { - List list = get(loc); - int s = list.size(); - get(loc).removeIf(predicate); - return s - list.size(); - } + /** + * Remove an element from the cell at the given location. + * + * @param loc The location of the grid cell + * @param predicate Predicate which should be true for elements to be removed + * @return Number of elements removed + * @throws IndexOutOfBoundsException if !isValid(loc) + */ + default int remove(Location loc, Predicate predicate) { + List list = get(loc); + int s = list.size(); + get(loc).removeIf(predicate); + return s - list.size(); + } - /** - * Remove an element from the cell at the given location. - * - * @param loc - * The location of the grid cell - * @param element - * An element to be removed from the cell. - * @return Number of elements removed - * @throws IndexOutOfBoundsException - * if !isValid(loc) - */ - default int remove(ILocation loc, T element) { - return get(loc).remove(element) ? 1 : 0; - } + /** + * Remove an element from the cell at the given location. + * + * @param loc The location of the grid cell + * @param element An element to be removed from the cell. + * @return Number of elements removed + * @throws IndexOutOfBoundsException if !isValid(loc) + */ + default int remove(Location loc, T element) { + return get(loc).remove(element) ? 1 : 0; + } - /** - * Remove an element from the cell at the given x,y location. - * - * y must be greater than or equal to 0 and less than getHeight(). x must be - * greater than or equal to 0 and less than getWidth(). - * - * @param pos - * The (x,y) position of the grid cell - * @param predicate - * Predicate which should be true for elements to be removed - * @return Number of elements removed - * @throws IndexOutOfBoundsException - * if !isValid(x,y) - */ - default int remove(int x, int y, Predicate predicate) { - return remove(getArea().location(x, y), predicate); - } + /** + * Remove an element from the cell at the given x,y location. + *

    + * y must be greater than or equal to 0 and less than getHeight(). x must be + * greater than or equal to 0 and less than getWidth(). + * + * @param pos The (x,y) position of the grid cell + * @param predicate Predicate which should be true for elements to be removed + * @return Number of elements removed + * @throws IndexOutOfBoundsException if !isValid(x,y) + */ + default int remove(int x, int y, Predicate predicate) { + return remove(getArea().location(x, y), predicate); + } - /** - * Remove an element from the cell at the given x,y location. - * - * y must be greater than or equal to 0 and less than getHeight(). x must be - * greater than or equal to 0 and less than getWidth(). - * - * @param pos - * The (x,y) position of the grid cell - * @param element - * An element to be removed from the cell - * @return Number of elements removed - * @throws IndexOutOfBoundsException - * if !isValid(x,y) - */ - default int remove(int x, int y, T element) { - return get(x, y).remove(element) ? 1 : 0; - } + /** + * Remove an element from the cell at the given x,y location. + *

    + * y must be greater than or equal to 0 and less than getHeight(). x must be + * greater than or equal to 0 and less than getWidth(). + * + * @param pos The (x,y) position of the grid cell + * @param element An element to be removed from the cell + * @return Number of elements removed + * @throws IndexOutOfBoundsException if !isValid(x,y) + */ + default int remove(int x, int y, T element) { + return get(x, y).remove(element) ? 1 : 0; + } } \ No newline at end of file diff --git a/src/main/java/inf101/v18/grid/IPosition.java b/src/main/java/inf101/v18/grid/IPosition.java index 2b430d6..49aa10f 100644 --- a/src/main/java/inf101/v18/grid/IPosition.java +++ b/src/main/java/inf101/v18/grid/IPosition.java @@ -2,82 +2,83 @@ package inf101.v18.grid; public interface IPosition { - /** - * @param obj - * Another object - * @return true if obj is also an IPosition, and the x and y coordinates are - * equal - */ - @Override - boolean equals(Object obj); + /** + * @param obj Another object + * @return true if obj is also an IPosition, and the x and y coordinates are + * equal + */ + @Override + boolean equals(Object obj); - /** - * Find the Euclidian distance between the midpoint of this position and another - * position. - * - * The distance is computed with the Pythagorean formula, with the assumption - * that the grid cells are square shaped with width = height = - * 1. For example, the distance from (0,0) to (3,5) is √(3²+5²) = 5.83. - * - * @param other - * @return Euclidian distance between this and other's midpoints - */ - double geometricDistanceTo(IPosition other); + /** + * Find the Euclidian distance between the midpoint of this position and another + * position. + *

    + * The distance is computed with the Pythagorean formula, with the assumption + * that the grid cells are square shaped with width = height = + * 1. For example, the distance from (0,0) to (3,5) is √(3²+5²) = 5.83. + * + * @param other + * @return Euclidian distance between this and other's midpoints + */ + double geometricDistanceTo(IPosition other); - /** - * Gets the x-coordinate - * - * @return - */ - int getX(); + /** + * Gets the x-coordinate + * + * @return + */ + int getX(); - /** - * Gets the y-coordinate - * - * @return - */ - int getY(); + /** + * Gets the y-coordinate + * + * @return + */ + int getY(); - /** - * Find the distance in grid cells to another location. - * - * This is different from {@link #stepDistanceTo(IPosition)} in that diagonal - * steps are allowed, and is the same as the number of steps a queen would take - * on a chess board. - *

    - * Computes the maximum of the horizontal and the vertical distance. For - * example, to go from (0,0) to (3,5), you could go three steps SOUTHEAST and - * two steps SOUTH, so the {@link #gridDistanceTo(IPosition)} is 5. - * - * @param other - * @return Number of horizontal/vertical/diagonal (queen-like) steps to - * other - */ - int gridDistanceTo(IPosition other); + /** + * Find the distance in grid cells to another location. + *

    + * This is different from {@link #stepDistanceTo(IPosition)} in that diagonal + * steps are allowed, and is the same as the number of steps a queen would take + * on a chess board. + *

    + * Computes the maximum of the horizontal and the vertical distance. For + * example, to go from (0,0) to (3,5), you could go three steps SOUTHEAST and + * two steps SOUTH, so the {@link #gridDistanceTo(IPosition)} is 5. + * + * @param other + * @return Number of horizontal/vertical/diagonal (queen-like) steps to + * other + */ + int gridDistanceTo(IPosition other); - @Override - int hashCode(); + @Override + int hashCode(); - /** - * Find the number of non-diagonal steps needed to go from this location the - * other location. - * - * This is different from {@link #gridDistanceTo(IPosition)} in that only - * non-diagonal steps are allowed, and is the same as the number of steps a rook - * would take on a chess board. - *

    - * Computes the distance to another location as the sum of the absolute - * difference between the x- and y-coordinates. For example, to go from (0,0) to - * (3,5), you would need to go three steps EAST and five steps SOUTH, so the - * {@link #stepDistanceTo(IPosition)} is 8. - * - * @param other - * @return Number of horizontal/vertical (rook-like) steps to other - */ - int stepDistanceTo(IPosition other); + /** + * Find the number of non-diagonal steps needed to go from this location the + * other location. + *

    + * This is different from {@link #gridDistanceTo(IPosition)} in that only + * non-diagonal steps are allowed, and is the same as the number of steps a rook + * would take on a chess board. + *

    + * Computes the distance to another location as the sum of the absolute + * difference between the x- and y-coordinates. For example, to go from (0,0) to + * (3,5), you would need to go three steps EAST and five steps SOUTH, so the + * {@link #stepDistanceTo(IPosition)} is 8. + * + * @param other + * @return Number of horizontal/vertical (rook-like) steps to other + */ + int stepDistanceTo(IPosition other); - /** @return Position as a string, "(x,y)" */ - @Override - String toString(); + /** + * @return Position as a string, "(x,y)" + */ + @Override + String toString(); } \ No newline at end of file diff --git a/src/main/java/inf101/v18/grid/Location.java b/src/main/java/inf101/v18/grid/Location.java new file mode 100644 index 0000000..409fe4c --- /dev/null +++ b/src/main/java/inf101/v18/grid/Location.java @@ -0,0 +1,104 @@ +package inf101.v18.grid; + +import java.util.Collection; +import java.util.List; + +/** + * Represents a location within an {@link IArea}. + *

    + * Immutable: Locations are immutable; i.e., a particular + * ILocation will always have the same x- and y-coordinates and + * cannot be changed. Methods that find neighbours (such as + * {@link #go(GridDirection)}) will return another ILocation. + *

    + * Area: All {@link Location}s are tied to an {@link IArea}, thus they + * will know whether they are on the edge of the area (and not all neighbouring + * grid coordinates will be valid locations within the area) and other + * coordinate system properties that are determined by the {@link IArea} (e.g., + * if the coordinate system wraps around like on the surface of a donut). + *

    + * Unique objects: All {@link Location} in an {@link IArea} are + * unique. This means that area.location(x,y) == area.location(x,y) + * for all x and y, and that: + * + *

    + * // equality is reference equality for locations in the same area
    + * if (l1.getArea() == l2.getArea())
    + * 	assertEquals(l1.equals(l2), (l1 == l2));
    + * 
    + * + * @author Anya Helene Bagge, UiB + */ +public interface Location extends IPosition { + + /** + * Iterate over neighbours in eight directions. + *

    + * (The iterator may yield fewer than eight locations if the current location is + * at the edge of its containing area. + * + * @return The neighbours in the eight cardinal and intercardinal directions + * ({@link GridDirection#NORTH}, @link GridDirection#SOUTH}, @link + * GridDirection#EAST}, @link GridDirection#WEST}, + * {@link GridDirection#NORTHEAST}, @link + * GridDirection#NORTHWEST}, @link GridDirection#SOUTHEAST}, @link + * GridDirection#SOUTHWEST}, ) + */ + Collection allNeighbours(); + + /** + * Checks whether you can go towards direction dir without going outside the + * area bounds + * + * @param dir + * @return True if go(dir) will succeed + */ + boolean canGo(GridDirection dir); + + /** + * Iterate over north/south/east/west neighbours. + *

    + * (The iterator may yield fewer than four locations if the current location is + * at the edge of its containing area. + * + * @return The neighbours in the four cardinal directions + * ({@link GridDirection#NORTH}, @link GridDirection#SOUTH}, @link + * GridDirection#EAST}, @link GridDirection#WEST} + */ + Collection cardinalNeighbours(); + + IArea getArea(); + + int getIndex(); + + /** + * Return the next location in direction dir. + *

    + * This does not change the location object; rather it returns the + * ILocation you would end up at if you go in the given direction from this + * ILocation. + * + * @param dir A direction + * @return A neighbouring location + * @throws IndexOutOfBoundsException if !canGo(dir) + */ + Location go(GridDirection dir); + + /** + * Find the grid cells between this location (exclusive) and another location + * (inclusive). + *

    + * This is a list of length {@link #gridDistanceTo(other)}, containing + * the cells that a chess queen would visit when moving to other. + *

    + * Computes the maximum of the horizontal and the vertical distance. For + * example, to go from (0,0) to (3,5), you could go three steps SOUTHEAST and + * two steps SOUTH, so the {@link #gridDistanceTo(IPosition)} is 5. + * + * @param other + * @return Number of horizontal/vertical/diagonal (queen-like) steps to + * other + */ + List gridLineTo(Location other); + +} \ No newline at end of file diff --git a/src/main/java/inf101/v18/grid/MultiGrid.java b/src/main/java/inf101/v18/grid/MultiGrid.java index f088af4..dd9dc2b 100644 --- a/src/main/java/inf101/v18/grid/MultiGrid.java +++ b/src/main/java/inf101/v18/grid/MultiGrid.java @@ -5,12 +5,12 @@ import java.util.List; public class MultiGrid extends MyGrid> implements IMultiGrid { - public MultiGrid(IArea area) { - super(area, (l) -> new ArrayList()); - } + public MultiGrid(IArea area) { + super(area, (l) -> new ArrayList()); + } - public MultiGrid(int width, int height) { - super(width, height, (l) -> new ArrayList()); - } + public MultiGrid(int width, int height) { + super(width, height, (l) -> new ArrayList()); + } } diff --git a/src/main/java/inf101/v18/grid/MyGrid.java b/src/main/java/inf101/v18/grid/MyGrid.java index afa9a9c..bd46f6a 100644 --- a/src/main/java/inf101/v18/grid/MyGrid.java +++ b/src/main/java/inf101/v18/grid/MyGrid.java @@ -6,216 +6,219 @@ import java.util.List; import java.util.function.Function; import java.util.stream.Stream; -/** A Grid contains a set of cells in a square 2D matrix. */ +/** + * A Grid contains a set of cells in a square 2D matrix. + */ public class MyGrid implements IGrid { - private final IArea area; - private final List cells; + private final IArea area; + private final List cells; - /** - * Construct a grid with the given dimensions. - * - * The initialiser function will be called with the (x,y) position of an - * element, and is expected to return the element to place at that position. For - * example: - * - *

    -	 * // fill all cells with the position as a string (e.g., "(2,2)")
    -	 * new MyGrid(10, 10, ((x, y) -> String.format("(%d,%d)", x, y));
    -	 * 
    - * - * @param area - * @param initialiser - * The initialiser function - */ - public MyGrid(IArea area, Function initialiser) { - if (area == null || initialiser == null) { - throw new IllegalArgumentException(); - } + /** + * Construct a grid with the given dimensions. + *

    + * The initialiser function will be called with the (x,y) position of an + * element, and is expected to return the element to place at that position. For + * example: + * + *

    +     * // fill all cells with the position as a string (e.g., "(2,2)")
    +     * new MyGrid(10, 10, ((x, y) -> String.format("(%d,%d)", x, y));
    +     * 
    + * + * @param area + * @param initialiser The initialiser function + */ + public MyGrid(IArea area, Function initialiser) { + if (area == null || initialiser == null) { + throw new IllegalArgumentException(); + } - this.area = area; - this.cells = new ArrayList(area.getSize()); - for (ILocation loc : area) { - cells.add(initialiser.apply(loc)); - } - } + this.area = area; + this.cells = new ArrayList(area.getSize()); + for (Location loc : area) { + cells.add(initialiser.apply(loc)); + } + } - /** - * Construct a grid with the given dimensions. - * - * @param area - * @param initElement - * What the cells should initially hold (possibly null) - */ - public MyGrid(IArea area, T initElement) { - if (area == null) { - throw new IllegalArgumentException(); - } + /** + * Construct a grid with the given dimensions. + * + * @param area + * @param initElement What the cells should initially hold (possibly null) + */ + public MyGrid(IArea area, T initElement) { + if (area == null) { + throw new IllegalArgumentException(); + } - this.area = area; - this.cells = new ArrayList(area.getSize()); - for (int i = 0; i < area.getSize(); ++i) { - cells.add(initElement); - } - } + this.area = area; + this.cells = new ArrayList(area.getSize()); + for (int i = 0; i < area.getSize(); ++i) { + cells.add(initElement); + } + } - /** - * Construct a grid with the given dimensions. - * - * The initialiser function will be called with the (x,y) position of an - * element, and is expected to return the element to place at that position. For - * example: - * - *
    -	 * // fill all cells with the position as a string (e.g., "(2,2)")
    -	 * new MyGrid(10, 10, ((x, y) -> String.format("(%d,%d)", x, y));
    -	 * 
    - * - * @param width - * @param height - * @param initialiser - * The initialiser function - */ - public MyGrid(int width, int height, Function initialiser) { - this(new RectArea(width, height), initialiser); - } + /** + * Construct a grid with the given dimensions. + *

    + * The initialiser function will be called with the (x,y) position of an + * element, and is expected to return the element to place at that position. For + * example: + * + *

    +     * // fill all cells with the position as a string (e.g., "(2,2)")
    +     * new MyGrid(10, 10, ((x, y) -> String.format("(%d,%d)", x, y));
    +     * 
    + * + * @param width + * @param height + * @param initialiser The initialiser function + */ + public MyGrid(int width, int height, Function initialiser) { + this(new RectArea(width, height), initialiser); + } - /** - * Construct a grid with the given dimensions. - * - * @param width - * @param height - * @param initElement - * What the cells should initially hold (possibly null) - */ - public MyGrid(int width, int height, T initElement) { - this(new RectArea(width, height), initElement); - } + /** + * Construct a grid with the given dimensions. + * + * @param width + * @param height + * @param initElement What the cells should initially hold (possibly null) + */ + public MyGrid(int width, int height, T initElement) { + this(new RectArea(width, height), initElement); + } - @Override - public IGrid copy() { - return new MyGrid<>(getWidth(), getHeight(), (l) -> get(l)); - } + @Override + public IGrid copy() { + return new MyGrid<>(getWidth(), getHeight(), (l) -> get(l)); + } - @Override - public Stream elementParallelStream() { - return cells.parallelStream(); - } + @Override + public Stream elementParallelStream() { + return cells.parallelStream(); + } - @Override - public Stream elementStream() { - return cells.stream(); - } + @Override + public Stream elementStream() { + return cells.stream(); + } - @Override - public void fill(Function initialiser) { - if (initialiser == null) - throw new NullPointerException(); + @Override + public void fill(Function initialiser) { + if (initialiser == null) { + throw new NullPointerException(); + } - for (int i = 0; i < area.getSize(); i++) { - cells.set(i, initialiser.apply(area.fromIndex(i))); - } - } + for (int i = 0; i < area.getSize(); i++) { + cells.set(i, initialiser.apply(area.fromIndex(i))); + } + } - @Override - public void fill(T element) { - for (int i = 0; i < area.getSize(); i++) { - cells.set(i, element); - } - } + @Override + public void fill(T element) { + for (int i = 0; i < area.getSize(); i++) { + cells.set(i, element); + } + } - @Override - public T get(ILocation loc) { - if (loc.getArea() == area) - return cells.get(loc.getIndex()); - else - return cells.get(area.toIndex(loc.getX(), loc.getY())); - } + @Override + public T get(Location loc) { + if (loc.getArea() == area) { + return cells.get(loc.getIndex()); + } else { + return cells.get(area.toIndex(loc.getX(), loc.getY())); + } + } - @Override - public T get(int x, int y) { - return cells.get(area.toIndex(x, y)); - } + @Override + public T get(int x, int y) { + return cells.get(area.toIndex(x, y)); + } - @Override - public IArea getArea() { - return area; - } + @Override + public IArea getArea() { + return area; + } - @Override - public int getHeight() { - return area.getHeight(); - } + @Override + public int getHeight() { + return area.getHeight(); + } - @Override - public T getOrDefault(ILocation loc, T defaultResult) { - if (loc.getArea() == area) { - T r = cells.get(loc.getIndex()); - if (r != null) - return r; - else - return defaultResult; - } else { - return getOrDefault(loc.getX(), loc.getY(), defaultResult); - } - } + @Override + public T getOrDefault(Location loc, T defaultResult) { + if (loc.getArea() == area) { + T r = cells.get(loc.getIndex()); + if (r != null) { + return r; + } else { + return defaultResult; + } + } else { + return getOrDefault(loc.getX(), loc.getY(), defaultResult); + } + } - @Override - public T getOrDefault(int x, int y, T defaultResult) { - T r = null; - if (isValid(x, y)) - r = get(x, y); - if (r != null) - return r; - else - return defaultResult; - } + @Override + public T getOrDefault(int x, int y, T defaultResult) { + T r = null; + if (isValid(x, y)) { + r = get(x, y); + } + if (r != null) { + return r; + } else { + return defaultResult; + } + } - @Override - public int getWidth() { - return area.getWidth(); - } + @Override + public int getWidth() { + return area.getWidth(); + } - @Override - public boolean isValid(ILocation loc) { - return loc.getArea() == area || area.contains(loc.getX(), loc.getY()); - } + @Override + public boolean isValid(Location loc) { + return loc.getArea() == area || area.contains(loc.getX(), loc.getY()); + } - @Override - public boolean isValid(int x, int y) { - return area.contains(x, y); - } + @Override + public boolean isValid(int x, int y) { + return area.contains(x, y); + } - @Override - public Iterator iterator() { - return cells.iterator(); - } + @Override + public Iterator iterator() { + return cells.iterator(); + } - @Override - public Stream locationParallelStream() { - return area.parallelStream(); - } + @Override + public Stream locationParallelStream() { + return area.parallelStream(); + } - @Override - public Iterable locations() { - return area; - } + @Override + public Iterable locations() { + return area; + } - @Override - public Stream locationStream() { - return area.stream(); - } + @Override + public Stream locationStream() { + return area.stream(); + } - @Override - public void set(ILocation loc, T element) { - if (loc.getArea() == area) { - cells.set(loc.getIndex(), element); - } else { - set(loc.getX(), loc.getY(), element); - } - } + @Override + public void set(Location loc, T element) { + if (loc.getArea() == area) { + cells.set(loc.getIndex(), element); + } else { + set(loc.getX(), loc.getY(), element); + } + } - @Override - public void set(int x, int y, T elem) { - cells.set(area.toIndex(x, y), elem); - } + @Override + public void set(int x, int y, T elem) { + cells.set(area.toIndex(x, y), elem); + } } diff --git a/src/main/java/inf101/v18/grid/README-GRID.md b/src/main/java/inf101/v18/grid/README-GRID.md index 018f81c..831b274 100644 --- a/src/main/java/inf101/v18/grid/README-GRID.md +++ b/src/main/java/inf101/v18/grid/README-GRID.md @@ -2,9 +2,21 @@ ## Changes from the Cellular Automaton version -* If you look at `MyGrid.java`, you will notice that it no longer uses `IList`/`MyList` to hold the grid cells, and both `IList.java` and `MyList.java` have been removed. Instead we use standard built-in Java lists, with the `List` interface (and `ArrayList` as the class for new list objects). - * This saves us the work of maintaining and improving our own list, and makes it immediately compatible with useful standard Java library operations on lists. - * In general, you should always use built-in APIs (such as the Java Collections API which provides lists and similar) when available. Not only does it save time, but they're likely to be much better tested (and probably better designed) than you can do yourself, so they're less likely to have, e.g., stupid bugs that only show up in corner cases. You may also get better performance. - * You might want to do things yourself if you want to learn how stuff works; also, in some cases, there may not be a suitable API available – or the API doesn't really fully fit with what we want to do. While Java has one-dimensional lists and arrays, it doesn't really have built-in classes for grids, so we're making our own. - * An alternative to IGrid/MyGrid would be to use two-dimensional arrays – but these aren't *actually* two-dimensional arrays, but rather arrays-of-arrays, which makes them fit less well with the concept of a “grid” (the same goes for making a list of lists). In addition to being a bit inconvenient, they're also much less efficient in use – and they won't let us do useful things like “please give me the cell to the north of this cell” or “please give me all cells neighbouring this cell”. +* If you look at `MyGrid.java`, you will notice that it no longer uses `IList`/`MyList` to hold the grid cells, + and both `IList.java` and `MyList.java` have been removed. Instead we use standard built-in Java lists, with + the `List` interface (and `ArrayList` as the class for new list objects). + * This saves us the work of maintaining and improving our own list, and makes it immediately compatible with useful + standard Java library operations on lists. + * In general, you should always use built-in APIs (such as the Java Collections API which provides lists and + similar) when available. Not only does it save time, but they're likely to be much better tested (and probably + better designed) than you can do yourself, so they're less likely to have, e.g., stupid bugs that only show up in + corner cases. You may also get better performance. + * You might want to do things yourself if you want to learn how stuff works; also, in some cases, there may not be a + suitable API available – or the API doesn't really fully fit with what we want to do. While Java has + one-dimensional lists and arrays, it doesn't really have built-in classes for grids, so we're making our own. + * An alternative to IGrid/MyGrid would be to use two-dimensional arrays – but these aren't *actually* + two-dimensional arrays, but rather arrays-of-arrays, which makes them fit less well with the concept of a “grid” ( + the same goes for making a list of lists). In addition to being a bit inconvenient, they're also much less + efficient in use – and they won't let us do useful things like “please give me the cell to the north of this cell” + or “please give me all cells neighbouring this cell”. \ No newline at end of file diff --git a/src/main/java/inf101/v18/grid/RectArea.java b/src/main/java/inf101/v18/grid/RectArea.java index 7ceab6b..5555f25 100644 --- a/src/main/java/inf101/v18/grid/RectArea.java +++ b/src/main/java/inf101/v18/grid/RectArea.java @@ -8,334 +8,338 @@ import java.util.List; import java.util.stream.Stream; public class RectArea implements IArea { - /** A class to represent an (x, y)-location on a grid. */ - class Location implements ILocation { + /** + * A class to represent an (x, y)-location on a grid. + */ + class Location implements inf101.v18.grid.Location { - /** value of the x-coordinate */ - protected final int x; - /** value of the y-coordinate */ - protected final int y; - protected final int idx; - protected final int edgeMask; + /** + * value of the x-coordinate + */ + protected final int x; + /** + * value of the y-coordinate + */ + protected final int y; + protected final int idx; + protected final int edgeMask; - /** - * Main constructor. Initializes a new {@link #Location} objects with the - * corresponding values of x and y. - * - * @param x - * X coordinate - * @param y - * Y coordinate - * @param idx - * 1-dimensional index - * @param edgeMask - * mask with bits {@link RectArea#N}, {@link RectArea#S}, - * {@link RectArea#E}, {@link RectArea#W} set if we're on the - * corresponding edge of the area - */ - Location(int x, int y, int idx, int edgeMask) { - this.x = x; - this.y = y; - this.idx = idx; - this.edgeMask = edgeMask; - } + /** + * Main constructor. Initializes a new {@link #Location} objects with the + * corresponding values of x and y. + * + * @param x X coordinate + * @param y Y coordinate + * @param idx 1-dimensional index + * @param edgeMask mask with bits {@link RectArea#N}, {@link RectArea#S}, + * {@link RectArea#E}, {@link RectArea#W} set if we're on the + * corresponding edge of the area + */ + Location(int x, int y, int idx, int edgeMask) { + this.x = x; + this.y = y; + this.idx = idx; + this.edgeMask = edgeMask; + } - @Override - public Collection allNeighbours() { - Collection ns = new ArrayList<>(8); - for (GridDirection d : GridDirection.EIGHT_DIRECTIONS) { - if (canGo(d)) - ns.add(go(d)); - } - return ns; - } + @Override + public Collection allNeighbours() { + Collection ns = new ArrayList<>(8); + for (GridDirection d : GridDirection.EIGHT_DIRECTIONS) { + if (canGo(d)) { + ns.add(go(d)); + } + } + return ns; + } - @Override - public boolean canGo(GridDirection dir) { - return (edgeMask & dir.getMask()) == 0; - } + @Override + public boolean canGo(GridDirection dir) { + return (edgeMask & dir.getMask()) == 0; + } - @Override - public Collection cardinalNeighbours() { - Collection ns = new ArrayList<>(4); - for (GridDirection d : GridDirection.FOUR_DIRECTIONS) { - if (canGo(d)) - ns.add(go(d)); - } - return ns; - } + @Override + public Collection cardinalNeighbours() { + Collection ns = new ArrayList<>(4); + for (GridDirection d : GridDirection.FOUR_DIRECTIONS) { + if (canGo(d)) { + ns.add(go(d)); + } + } + return ns; + } - @Override - public boolean equals(Object obj) { - if (this == obj) { - return true; - } - if (obj == null) { - return false; - } - if (!(obj instanceof IPosition)) { - return false; - } - IPosition other = (IPosition) obj; - if (x != other.getX()) { - return false; - } - return y == other.getY(); - } + @Override + public boolean equals(Object obj) { + if (this == obj) { + return true; + } + if (obj == null) { + return false; + } + if (!(obj instanceof IPosition)) { + return false; + } + IPosition other = (IPosition) obj; + if (x != other.getX()) { + return false; + } + return y == other.getY(); + } - @Override - public double geometricDistanceTo(IPosition other) { - return Math.sqrt(Math.pow(this.x - other.getX(), 2) + Math.pow(this.y - other.getY(), 2)); - } + @Override + public double geometricDistanceTo(IPosition other) { + return Math.sqrt(Math.pow(this.x - other.getX(), 2) + Math.pow(this.y - other.getY(), 2)); + } - @Override - public IArea getArea() { - return RectArea.this; - } + @Override + public IArea getArea() { + return RectArea.this; + } - @Override - public int getIndex() { - return idx; - } + @Override + public int getIndex() { + return idx; + } - @Override - public int getX() { - return x; - } + @Override + public int getX() { + return x; + } - @Override - public int getY() { - return y; - } + @Override + public int getY() { + return y; + } - @Override - public ILocation go(GridDirection dir) { - return location(x + dir.getDx(), y + dir.getDy()); - } + @Override + public inf101.v18.grid.Location go(GridDirection dir) { + return location(x + dir.getDx(), y + dir.getDy()); + } - @Override - public int gridDistanceTo(IPosition other) { - return Math.max(Math.abs(this.x - other.getX()), Math.abs(this.y - other.getY())); - } + @Override + public int gridDistanceTo(IPosition other) { + return Math.max(Math.abs(this.x - other.getX()), Math.abs(this.y - other.getY())); + } - @Override - public List gridLineTo(ILocation other) { - if (!contains(other)) - throw new IllegalArgumentException(); - int distX = other.getX() - x; - int distY = other.getY() - y; - int length = Math.max(Math.abs(distX), Math.abs(distY)); - List line = new ArrayList<>(length); - if (length == 0) - return line; - double dx = (double) distX / (double) length; - double dy = (double) distY / (double) length; - // System.out.printf("dx=%g, dy=%g, length=%d%n", dx, dy, length); - for (int i = 1; i <= length; i++) { - line.add(location(x + (int) Math.round(dx * i), y + (int) Math.round(dy * i))); - } - return line; - } + @Override + public List gridLineTo(inf101.v18.grid.Location other) { + if (!contains(other)) { + throw new IllegalArgumentException(); + } + int distX = other.getX() - x; + int distY = other.getY() - y; + int length = Math.max(Math.abs(distX), Math.abs(distY)); + List line = new ArrayList<>(length); + if (length == 0) { + return line; + } + double dx = (double) distX / (double) length; + double dy = (double) distY / (double) length; + // System.out.printf("dx=%g, dy=%g, length=%d%n", dx, dy, length); + for (int i = 1; i <= length; i++) { + line.add(location(x + (int) Math.round(dx * i), y + (int) Math.round(dy * i))); + } + return line; + } - @Override - public int hashCode() { - final int prime = 31; - int result = 1; - result = prime * result + x; - result = prime * result + y; - return result; - } + @Override + public int hashCode() { + final int prime = 31; + int result = 1; + result = prime * result + x; + result = prime * result + y; + return result; + } - @Override - public int stepDistanceTo(IPosition other) { - return Math.abs(this.x - other.getX()) + Math.abs(this.y - other.getY()); - } + @Override + public int stepDistanceTo(IPosition other) { + return Math.abs(this.x - other.getX()) + Math.abs(this.y - other.getY()); + } - @Override - public String toString() { - return "(x=" + x + ",y=" + y + ")"; - } + @Override + public String toString() { + return "(x=" + x + ",y=" + y + ")"; + } - } + } - protected final int width; - protected final int height; - protected final int size; - protected final List locs; + protected final int width; + protected final int height; + protected final int size; + protected final List locs; - protected final boolean hWrap, vWrap; + protected final boolean hWrap, vWrap; - public RectArea(int width, int height) { - this(width, height, false, false); - } + public RectArea(int width, int height) { + this(width, height, false, false); + } - private RectArea(int width, int height, boolean horizWrap, boolean vertWrap) { - if (width < 1 || height < 1) { - throw new IllegalArgumentException("Width and height must be positive"); - } - this.hWrap = horizWrap; - this.vWrap = vertWrap; - this.width = width; - this.height = height; - this.size = width * height; - List l = new ArrayList<>(size); - for (int y = 0, i = 0; y < height; y++) { - // set N or S bits if we're on the northern or southern edge - int edge = (y == 0 ? GridDirection.NORTH.getMask() : 0) - | (y == height - 1 ? GridDirection.SOUTH.getMask() : 0); - for (int x = 0; x < width; x++, i++) { - // set W or E bits if we're on the western or eastern edge - int e = edge | (x == 0 ? GridDirection.WEST.getMask() : 0) - | (x == width - 1 ? GridDirection.EAST.getMask() : 0); - l.add(new Location(x, y, i, e)); - } - } - locs = Collections.unmodifiableList(l); - } + private RectArea(int width, int height, boolean horizWrap, boolean vertWrap) { + if (width < 1 || height < 1) { + throw new IllegalArgumentException("Width and height must be positive"); + } + this.hWrap = horizWrap; + this.vWrap = vertWrap; + this.width = width; + this.height = height; + this.size = width * height; + List l = new ArrayList<>(size); + for (int y = 0, i = 0; y < height; y++) { + // set N or S bits if we're on the northern or southern edge + int edge = (y == 0 ? GridDirection.NORTH.getMask() : 0) + | (y == height - 1 ? GridDirection.SOUTH.getMask() : 0); + for (int x = 0; x < width; x++, i++) { + // set W or E bits if we're on the western or eastern edge + int e = edge | (x == 0 ? GridDirection.WEST.getMask() : 0) + | (x == width - 1 ? GridDirection.EAST.getMask() : 0); + l.add(new Location(x, y, i, e)); + } + } + locs = Collections.unmodifiableList(l); + } - /** - * @param x - * X-coordinate - * @return The same x, wrapped to wrapX(x) - * @throws IndexOutOfBoundsException - * if coordinate is out of range, and wrapping is not enabled - */ - protected int checkX(int x) { - x = wrapX(x); - if (x < 0 || x >= width) { - throw new IndexOutOfBoundsException("x=" + x); - } + /** + * @param x X-coordinate + * @return The same x, wrapped to wrapX(x) + * @throws IndexOutOfBoundsException if coordinate is out of range, and wrapping is not enabled + */ + protected int checkX(int x) { + x = wrapX(x); + if (x < 0 || x >= width) { + throw new IndexOutOfBoundsException("x=" + x); + } - return x; - } + return x; + } - /** - * @param y - * Y-coordinate - * @return The same y, wrapped to wrapY(y) - * @throws IndexOutOfBoundsException - * if coordinate is out of range, and wrapping is not enabled - */ - protected int checkY(int y) { - y = wrapY(y); - if (y < 0 || y >= height) { - throw new IndexOutOfBoundsException("y=" + y); - } - return y; - } + /** + * @param y Y-coordinate + * @return The same y, wrapped to wrapY(y) + * @throws IndexOutOfBoundsException if coordinate is out of range, and wrapping is not enabled + */ + protected int checkY(int y) { + y = wrapY(y); + if (y < 0 || y >= height) { + throw new IndexOutOfBoundsException("y=" + y); + } + return y; + } - @Override - public boolean contains(int x, int y) { - x = wrapX(x); - y = wrapY(y); - return x >= 0 && x < width && y >= 0 && y < height; - } + @Override + public boolean contains(int x, int y) { + x = wrapX(x); + y = wrapY(y); + return x >= 0 && x < width && y >= 0 && y < height; + } - @Override - public boolean contains(IPosition pos) { - return (pos instanceof ILocation && ((ILocation) pos).getArea() == this) || contains(pos.getX(), pos.getY()); - } + @Override + public boolean contains(IPosition pos) { + return (pos instanceof inf101.v18.grid.Location && ((inf101.v18.grid.Location) pos).getArea() == this) || contains(pos.getX(), pos.getY()); + } - @Override - public ILocation fromIndex(int i) { - if (i >= 0 && i < size) - return locs.get(i); - else - throw new IndexOutOfBoundsException("" + i); - } + @Override + public inf101.v18.grid.Location fromIndex(int i) { + if (i >= 0 && i < size) { + return locs.get(i); + } else { + throw new IndexOutOfBoundsException("" + i); + } + } - @Override - public int getHeight() { - return height; - } + @Override + public int getHeight() { + return height; + } - @Override - public int getSize() { - return size; - } + @Override + public int getSize() { + return size; + } - @Override - public int getWidth() { - return width; - } + @Override + public int getWidth() { + return width; + } - @Override - public Iterator iterator() { - return locs.iterator(); - } + @Override + public Iterator iterator() { + return locs.iterator(); + } - @Override - public ILocation location(int x, int y) { - if (x < 0 || x >= width || y < 0 || y >= height) - throw new IndexOutOfBoundsException("(" + x + "," + y + ")"); - int i = x + y * width; - return locs.get(i); - } + @Override + public inf101.v18.grid.Location location(int x, int y) { + if (x < 0 || x >= width || y < 0 || y >= height) { + throw new IndexOutOfBoundsException("(" + x + "," + y + ")"); + } + int i = x + y * width; + return locs.get(i); + } - @Override - public List locations() { - return locs; // (OK since locs has been through Collections.unmodifiableList()) - } + @Override + public List locations() { + return locs; // (OK since locs has been through Collections.unmodifiableList()) + } - @Override - public Iterable neighboursOf(ILocation pos) { - return pos.allNeighbours(); - } + @Override + public Iterable neighboursOf(inf101.v18.grid.Location pos) { + return pos.allNeighbours(); + } - @Override - public Stream parallelStream() { - return locs.parallelStream(); - } + @Override + public Stream parallelStream() { + return locs.parallelStream(); + } - @Override - public Stream stream() { - return locs.stream(); - } + @Override + public Stream stream() { + return locs.stream(); + } - @Override - public int toIndex(int x, int y) { - x = checkX(x); - y = checkY(y); - return y * width + x; - } + @Override + public int toIndex(int x, int y) { + x = checkX(x); + y = checkY(y); + return y * width + x; + } - @Override - public String toString() { - return "RectArea [width=" + width + ", height=" + height + ", hWrap=" + hWrap + ", vWrap=" + vWrap + "]"; - } + @Override + public String toString() { + return "RectArea [width=" + width + ", height=" + height + ", hWrap=" + hWrap + ", vWrap=" + vWrap + "]"; + } - @Override - public boolean wrapsHorizontally() { - return hWrap; - } + @Override + public boolean wrapsHorizontally() { + return hWrap; + } - @Override - public boolean wrapsVertically() { - return vWrap; - } + @Override + public boolean wrapsVertically() { + return vWrap; + } - protected int wrapX(int x) { - if (hWrap) { - if (x < 0) { - return getWidth() + x % getWidth(); - } else { - return x % getWidth(); - } - } else { - return x; - } - } + protected int wrapX(int x) { + if (hWrap) { + if (x < 0) { + return getWidth() + x % getWidth(); + } else { + return x % getWidth(); + } + } else { + return x; + } + } - protected int wrapY(int y) { - if (hWrap) { - if (y < 0) { - return getHeight() + y % getHeight(); - } else { - return y % getHeight(); - } - } else { - return y; - } - } + protected int wrapY(int y) { + if (hWrap) { + if (y < 0) { + return getHeight() + y % getHeight(); + } else { + return y % getHeight(); + } + } else { + return y; + } + } } diff --git a/src/main/java/inf101/v18/rogue101/AppInfo.java b/src/main/java/inf101/v18/rogue101/AppInfo.java index 8cbab69..90e29c0 100644 --- a/src/main/java/inf101/v18/rogue101/AppInfo.java +++ b/src/main/java/inf101/v18/rogue101/AppInfo.java @@ -4,29 +4,29 @@ import java.util.Arrays; import java.util.List; public class AppInfo { - /** - * Your application name. - */ - public static final String APP_NAME = "Not Really Rogue"; - /** - * Your name. - */ - public static final String APP_DEVELOPER = "Kristian Knarvik (kkn015)"; - /** - * A short description. - */ - public static final String APP_DESCRIPTION = "Implementasjon av inf101.v18.sem1"; - /** - * List of extra credits (e.g. for media sources) - */ - public static final List APP_EXTRA_CREDITS = Arrays.asList( - "Sounds by Mike Koenig, Stephan Schutze and Mark DiAngelo", - "Thanks to Stian Johannesen Husum and Henning Berge for exchanging ideas" - ); - /** - * Help text. Could be used for an in-game help page, perhaps. - *

    - * Use \n for new lines, \f between pages (if multi-page). - */ - public static final String APP_HELP = ""; + /** + * Your application name. + */ + public static final String APP_NAME = "Not Really Rogue"; + /** + * Your name. + */ + public static final String APP_DEVELOPER = "Kristian Knarvik (kkn015)"; + /** + * A short description. + */ + public static final String APP_DESCRIPTION = "Implementasjon av inf101.v18.sem1"; + /** + * List of extra credits (e.g. for media sources) + */ + public static final List APP_EXTRA_CREDITS = Arrays.asList( + "Sounds by Mike Koenig, Stephan Schutze and Mark DiAngelo", + "Thanks to Stian Johannesen Husum and Henning Berge for exchanging ideas" + ); + /** + * Help text. Could be used for an in-game help page, perhaps. + *

    + * Use \n for new lines, \f between pages (if multi-page). + */ + public static final String APP_HELP = ""; } diff --git a/src/main/java/inf101/v18/rogue101/Launcher.java b/src/main/java/inf101/v18/rogue101/Launcher.java index 32e8d8e..6e8dfa8 100644 --- a/src/main/java/inf101/v18/rogue101/Launcher.java +++ b/src/main/java/inf101/v18/rogue101/Launcher.java @@ -3,7 +3,9 @@ package inf101.v18.rogue101; import javafx.application.Application; public class Launcher { + public static void main(String[] args) { Application.launch(Main.class, args); } + } diff --git a/src/main/java/inf101/v18/rogue101/Main.java b/src/main/java/inf101/v18/rogue101/Main.java index 220a53e..c161f8f 100644 --- a/src/main/java/inf101/v18/rogue101/Main.java +++ b/src/main/java/inf101/v18/rogue101/Main.java @@ -1,13 +1,10 @@ package inf101.v18.rogue101; -import java.io.PrintWriter; -import java.io.StringWriter; - import inf101.v18.gfx.Screen; import inf101.v18.gfx.gfxmode.ITurtle; import inf101.v18.gfx.textmode.Printer; import inf101.v18.gfx.textmode.TextMode; -import inf101.v18.rogue101.game.Game; +import inf101.v18.rogue101.game.RogueGame; import javafx.animation.KeyFrame; import javafx.animation.Timeline; import javafx.application.Application; @@ -17,112 +14,118 @@ import javafx.scene.input.KeyEvent; import javafx.stage.Stage; import javafx.util.Duration; +import java.io.PrintWriter; +import java.io.StringWriter; + public class Main extends Application { - // you might want to tune these options - public static final boolean MAIN_USE_BACKGROUND_GRID = false; - public static final boolean MAP_AUTO_SCALE_ITEM_DRAW = true; - public static final boolean MAP_DRAW_ONLY_DIRTY_CELLS = false; - public static final TextMode MAIN_TEXT_MODE = TextMode.MODE_80X25; - public static final boolean DEBUG_TIME = false; + // you might want to tune these options + public static final boolean MAIN_USE_BACKGROUND_GRID = false; + public static final boolean MAP_AUTO_SCALE_ITEM_DRAW = true; + public static final boolean MAP_DRAW_ONLY_DIRTY_CELLS = false; + public static final TextMode MAIN_TEXT_MODE = TextMode.MODE_80X25; - public static final int LINE_MAP_BOTTOM = 20; - public static final int LINE_STATUS = 21; - public static final int LINE_MSG1 = 22; - public static final int LINE_MSG2 = 23; - public static final int LINE_MSG3 = 24; - public static final int LINE_DEBUG = 25; - public static final int COLUMN_MAP_END = 40; - public static final int COLUMN_RIGHTSIDE_START = 43; + public static final boolean DEBUG_TIME = false; + public static final int LINE_STATUS = 21; + public static final int LINE_MSG1 = 22; + public static final int LINE_MSG2 = 23; + public static final int LINE_MSG3 = 24; + public static final int LINE_DEBUG = 25; + public static final int COLUMN_MAP_END = 40; + public static final int COLUMN_RIGHT_SIDE_START = 43; - public static void main(String[] args) { - launch(args); - } + public static void main(String[] args) { + launch(args); + } - private Screen screen; - private ITurtle painter; - private Printer printer; + private Screen screen; + private ITurtle painter; + private Printer printer; - private Game game; + private RogueGame game; - private boolean grid = MAIN_USE_BACKGROUND_GRID; - private boolean autoNextTurn = false; - private Timeline bigStepTimeline; - private Timeline smallStepTimeline; + private boolean grid = MAIN_USE_BACKGROUND_GRID; + private boolean autoNextTurn = false; + private Timeline bigStepTimeline; + private Timeline smallStepTimeline; - private void setup() { - // - game = new Game(screen, painter, printer); - game.draw(); + private void setup() { + // + game = new RogueGame(screen, painter, printer); + game.draw(); - // - bigStepTimeline = new Timeline(); - bigStepTimeline.setCycleCount(Timeline.INDEFINITE); - KeyFrame kf = new KeyFrame(Duration.millis(1000), (ActionEvent event) -> { - if (autoNextTurn) { - doTurn(); - } - }); - bigStepTimeline.getKeyFrames().add(kf); - // bigStepTimeline.playFromStart(); + // + bigStepTimeline = new Timeline(); + bigStepTimeline.setCycleCount(Timeline.INDEFINITE); + KeyFrame kf = new KeyFrame(Duration.millis(1000), (ActionEvent event) -> { + if (autoNextTurn) { + doTurn(); + } + }); + bigStepTimeline.getKeyFrames().add(kf); + // bigStepTimeline.playFromStart(); - // - smallStepTimeline = new Timeline(); - smallStepTimeline.setCycleCount(1); - kf = new KeyFrame(Duration.millis(1), (ActionEvent event) -> { - doTurn(); - }); - smallStepTimeline.getKeyFrames().add(kf); + // + smallStepTimeline = new Timeline(); + smallStepTimeline.setCycleCount(1); + kf = new KeyFrame(Duration.millis(1), (ActionEvent event) -> { + doTurn(); + }); + smallStepTimeline.getKeyFrames().add(kf); - // finally, start game - doTurn(); - } + // finally, start game + doTurn(); + } - @Override - public void start(Stage primaryStage) throws Exception { - screen = Screen.startPaintScene(primaryStage, Screen.CONFIG_PIXELS_STEP_SCALED); // Screen.CONFIG_SCREEN_FULLSCREEN_NO_HINT); + @Override + public void start(Stage primaryStage) throws Exception { + screen = Screen.startPaintScene(primaryStage, Screen.CONFIG_PIXELS_STEP_SCALED); // Screen.CONFIG_SCREEN_FULLSCREEN_NO_HINT); - printer = screen.createPrinter(); - painter = screen.createPainter(); - printer.setTextMode(MAIN_TEXT_MODE, true); - - // Font with emojis – need separate download - printer.setFont(Printer.FONT_SYMBOLA); - - if (grid) - printer.drawCharCells(); - printer.setAutoScroll(false); - screen.setKeyPressedHandler((KeyEvent event) -> { - KeyCode code = event.getCode(); - if (event.isShortcutDown()) { - if (code == KeyCode.Q) { - System.exit(0); - } else if (code == KeyCode.R) { - printer.cycleMode(true); - if (grid) - printer.drawCharCells(); - game.draw(); - printer.redrawDirty(); - return true; - } else if (code == KeyCode.A) { - screen.cycleAspect(); - if (grid) - printer.drawCharCells(); - return true; - } else if (code == KeyCode.G) { - grid = !grid; - if (grid) - printer.drawCharCells(); - else - screen.clearBackground(); - return true; - } else if (code == KeyCode.F) { - screen.setFullScreen(!screen.isFullScreen()); - return true; - } else if (code == KeyCode.L) { - printer.redrawTextPage(); - return true; - } + printer = screen.createPrinter(); + painter = screen.createPainter(); + printer.setTextMode(MAIN_TEXT_MODE, true); + + // Font with emojis – need separate download + printer.setFont(Printer.FONT_SYMBOLA); + + if (grid) { + printer.drawCharCells(); + } + printer.setAutoScroll(false); + screen.setKeyPressedHandler((KeyEvent event) -> { + KeyCode code = event.getCode(); + if (event.isShortcutDown()) { + if (code == KeyCode.Q) { + System.exit(0); + } else if (code == KeyCode.R) { + printer.cycleMode(true); + if (grid) { + printer.drawCharCells(); + } + game.draw(); + printer.redrawDirty(); + return true; + } else if (code == KeyCode.A) { + screen.cycleAspect(); + if (grid) { + printer.drawCharCells(); + } + return true; + } else if (code == KeyCode.G) { + grid = !grid; + if (grid) { + printer.drawCharCells(); + } else { + screen.clearBackground(); + } + return true; + } else if (code == KeyCode.F) { + screen.setFullScreen(!screen.isFullScreen()); + return true; + } else if (code == KeyCode.L) { + printer.redrawTextPage(); + return true; + } /*} else if (code == KeyCode.ENTER) { try { doTurn(); @@ -131,81 +134,83 @@ public class Main extends Application { e.printStackTrace(); } return true;*/ // This interferes with other code - } else { - try { - if (game.keyPressed(code)) { - game.draw(); + } else { + try { + if (game.keyPressed(code)) { + game.draw(); } else { - doTurn(); + doTurn(); } - } catch (Exception e) { - e.printStackTrace(); - try { - StringWriter sw = new StringWriter(); - PrintWriter writer = new PrintWriter(sw); - e.printStackTrace(writer); - writer.close(); - String trace = sw.toString().split("\n")[0]; - game.displayDebug("Exception: " + trace); - } catch (Exception e2) { - System.err.println("Also got this exception trying to display the previous one"); - e2.printStackTrace(); - game.displayDebug("Exception: " + e.getMessage()); - } - } - printer.redrawDirty(); - return true; - } - return false; - }); - /* - * screen.setKeyTypedHandler((KeyEvent event) -> { if (event.getCharacter() != - * KeyEvent.CHAR_UNDEFINED) { printer.print(event.getCharacter()); return true; - * } return false; }); - */ - setup(); + } catch (Exception e) { + e.printStackTrace(); + try { + StringWriter sw = new StringWriter(); + PrintWriter writer = new PrintWriter(sw); + e.printStackTrace(writer); + writer.close(); + String trace = sw.toString().split("\n")[0]; + game.displayDebug("Exception: " + trace); + } catch (Exception e2) { + System.err.println("Also got this exception trying to display the previous one"); + e2.printStackTrace(); + game.displayDebug("Exception: " + e.getMessage()); + } + } + printer.redrawDirty(); + return true; + } + return false; + }); + /* + * screen.setKeyTypedHandler((KeyEvent event) -> { if (event.getCharacter() != + * KeyEvent.CHAR_UNDEFINED) { printer.print(event.getCharacter()); return true; + * } return false; }); + */ + setup(); - primaryStage.show(); + primaryStage.show(); - } + } - public void doTurn() { - long t = System.currentTimeMillis(); - boolean waitForPlayer = game.doTurn(); - if (DEBUG_TIME) - System.out.println("doTurn() took " + (System.currentTimeMillis() - t) + "ms"); - long t2 = System.currentTimeMillis(); - game.draw(); - printer.redrawDirty(); - if (DEBUG_TIME) { - System.out.println("draw() took " + (System.currentTimeMillis() - t2) + "ms"); - System.out.println("doTurn()+draw() took " + (System.currentTimeMillis() - t) + "ms"); - System.out.println("waiting for player? " + waitForPlayer); - } - if (!waitForPlayer) - smallStepTimeline.playFromStart(); // this will kickstart a new turn in a few milliseconds - } + public void doTurn() { + long t = System.currentTimeMillis(); + boolean waitForPlayer = game.doTurn(); + if (DEBUG_TIME) { + System.out.println("doTurn() took " + (System.currentTimeMillis() - t) + "ms"); + } + long t2 = System.currentTimeMillis(); + game.draw(); + printer.redrawDirty(); + if (DEBUG_TIME) { + System.out.println("draw() took " + (System.currentTimeMillis() - t2) + "ms"); + System.out.println("doTurn()+draw() took " + (System.currentTimeMillis() - t) + "ms"); + System.out.println("waiting for player? " + waitForPlayer); + } + if (!waitForPlayer) { + smallStepTimeline.playFromStart(); // this will kickstart a new turn in a few milliseconds + } + } - public static String BUILTIN_MAP = "40 20\n" // - + "########################################\n" // - + "#...... ..C.R ......R.R......... ..R...#\n" // - + "#.R@R...... ..........RC..R...... ... .#\n" // - + "#.......... ..R......R.R..R........R...#\n" // - + "#R. R......... R..R.........R......R.RR#\n" // - + "#... ..R........R......R. R........R.RR#\n" // - + "###############################....R..R#\n" // - + "#. ...R..C. ..R.R..........C.RC....... #\n" // - + "#..C.....R..... ........RR R..R.....R..#\n" // - + "#...R..R.R..............R .R..R........#\n" // - + "#.R.....R........RRR.......R.. .C....R.#\n" // - + "#.C.. ..R. .....R.RC..C....R...R..C. .#\n" // - + "#. R..............R R..R........C.....R#\n" // - + "#........###############################\n" // - + "# R.........R...C....R.....R...R.......#\n" // - + "# R......... R..R........R......R.RR..##\n" // - + "#. ..R........R.....R. ....C...R.RR...#\n" // - + "#....RC..R........R......R.RC......R...#\n" // - + "#.C.... ..... ......... .R..R....R...R.#\n" // - + "########################################\n" // - ; + public static String BUILTIN_MAP = "40 20\n" // + + "########################################\n" // + + "#...... ..C.R ......R.R......... ..R...#\n" // + + "#.R@R...... ..........RC..R...... ... .#\n" // + + "#.......... ..R......R.R..R........R...#\n" // + + "#R. R......... R..R.........R......R.RR#\n" // + + "#... ..R........R......R. R........R.RR#\n" // + + "###############################....R..R#\n" // + + "#. ...R..C. ..R.R..........C.RC....... #\n" // + + "#..C.....R..... ........RR R..R.....R..#\n" // + + "#...R..R.R..............R .R..R........#\n" // + + "#.R.....R........RRR.......R.. .C....R.#\n" // + + "#.C.. ..R. .....R.RC..C....R...R..C. .#\n" // + + "#. R..............R R..R........C.....R#\n" // + + "#........###############################\n" // + + "# R.........R...C....R.....R...R.......#\n" // + + "# R......... R..R........R......R.RR..##\n" // + + "#. ..R........R.....R. ....C...R.RR...#\n" // + + "#....RC..R........R......R.RC......R...#\n" // + + "#.C.... ..... ......... .R..R....R...R.#\n" // + + "########################################\n" // + ; } diff --git a/src/main/java/inf101/v18/rogue101/enemies/Boss.java b/src/main/java/inf101/v18/rogue101/enemies/Boss.java index 29dbc81..3022b3a 100644 --- a/src/main/java/inf101/v18/rogue101/enemies/Boss.java +++ b/src/main/java/inf101/v18/rogue101/enemies/Boss.java @@ -1,29 +1,29 @@ package inf101.v18.rogue101.enemies; -import inf101.v18.grid.ILocation; import inf101.v18.rogue101.Main; import inf101.v18.rogue101.game.Game; -import inf101.v18.rogue101.game.IGame; -import inf101.v18.rogue101.items.Backpack; -import inf101.v18.rogue101.items.Sword; -import inf101.v18.rogue101.objects.IItem; -import inf101.v18.rogue101.objects.INonPlayer; -import inf101.v18.rogue101.shared.NPC; -import inf101.v18.rogue101.states.Attack; +import inf101.v18.rogue101.game.RogueGame; +import inf101.v18.rogue101.items.weapon.BasicSword; +import inf101.v18.rogue101.object.Item; +import inf101.v18.rogue101.state.AttackType; +import inf101.v18.util.NPCHelper; -public class Boss implements INonPlayer { - private int hp = getMaxHealth(); - private final Backpack backpack = new Backpack(); - private ILocation loc; +/** + * A boss enemy that's harder than any other enemy + */ +public class Boss extends Enemy { + /** + * Instantiates a new boss character + */ public Boss() { - backpack.add(new Sword()); + backpack.add(new BasicSword()); } @Override - public void doTurn(IGame game) { - loc = game.getLocation(); - NPC.tryAttack(game, 1, Attack.MELEE); + public void doTurn(Game game) { + currentLocation = game.getLocation(); + NPCHelper.tryAttack(game, 1, AttackType.MELEE); } @Override @@ -36,23 +36,13 @@ public class Boss implements INonPlayer { return 15; } - @Override - public IItem getItem(Class type) { - for (IItem item : backpack.getContent()) { - if (type.isInstance(item)) { - return item; - } - } - return null; - } - @Override public int getCurrentHealth() { return hp; } @Override - public int getDefence() { + public int getDefense() { return 40; } @@ -87,21 +77,13 @@ public class Boss implements INonPlayer { } @Override - public int handleDamage(IGame game, IItem source, int amount) { - hp -= amount; - if (hp < 0 && backpack.size() > 0) { //Will be useful in a dungeon with several bosses - boolean dropped = false; - for (IItem item : backpack.getContent()) { - if (game.dropAt(loc, item)) { - dropped = true; - } - } - if (dropped) { - game.displayMessage(getName() + " dropped something"); - } - ((Game)game).win(); + public int handleDamage(Game game, Item source, int amount) { + super.handleDamage(game, source, amount); + if (hp < 0) { + ((RogueGame) game).win(); } - game.getPrinter().printAt(Main.COLUMN_RIGHTSIDE_START, 19, "Boss HP: " + NPC.hpBar(this)); + game.getPrinter().printAt(Main.COLUMN_RIGHT_SIDE_START, 19, "Boss HP: " + NPCHelper.hpBar(this)); return amount; } + } diff --git a/src/main/java/inf101/v18/rogue101/enemies/Enemy.java b/src/main/java/inf101/v18/rogue101/enemies/Enemy.java new file mode 100644 index 0000000..47be867 --- /dev/null +++ b/src/main/java/inf101/v18/rogue101/enemies/Enemy.java @@ -0,0 +1,46 @@ +package inf101.v18.rogue101.enemies; + +import inf101.v18.grid.Location; +import inf101.v18.rogue101.game.Game; +import inf101.v18.rogue101.items.container.Backpack; +import inf101.v18.rogue101.object.Item; +import inf101.v18.rogue101.object.NonPlayerCharacter; +import inf101.v18.rogue101.state.Sound; + +public abstract class Enemy implements NonPlayerCharacter { + + protected int hp = getMaxHealth(); + protected final Backpack backpack = new Backpack(); + protected Location currentLocation; + + @Override + public Item getItem(Class type) { + for (Item item : backpack.getContent()) { + if (type.isInstance(item)) { + return item; + } + } + return null; + } + + @Override + public int handleDamage(Game game, Item source, int amount) { + hp -= amount; + if (hp < 0) { + Sound.NPC_DEATH.play(); + } + if (hp < 0 && backpack.size() > 0) { //Will be useful in a dungeon with several bosses + boolean dropped = false; + for (Item item : backpack.getContent()) { + if (game.dropAt(currentLocation, item)) { + dropped = true; + } + } + if (dropped) { + game.displayMessage(getName() + " dropped something"); + } + } + return amount; + } + +} diff --git a/src/main/java/inf101/v18/rogue101/enemies/Girl.java b/src/main/java/inf101/v18/rogue101/enemies/Girl.java index 5429282..cc5ad1d 100644 --- a/src/main/java/inf101/v18/rogue101/enemies/Girl.java +++ b/src/main/java/inf101/v18/rogue101/enemies/Girl.java @@ -1,108 +1,127 @@ package inf101.v18.rogue101.enemies; import inf101.v18.grid.GridDirection; -import inf101.v18.rogue101.game.IGame; -import inf101.v18.rogue101.items.*; -import inf101.v18.rogue101.objects.IItem; -import inf101.v18.rogue101.objects.INonPlayer; -import inf101.v18.rogue101.shared.NPC; -import inf101.v18.rogue101.states.Age; -import inf101.v18.rogue101.states.Attack; -import inf101.v18.rogue101.states.Occupation; -import inf101.v18.rogue101.states.Personality; +import inf101.v18.grid.Location; +import inf101.v18.rogue101.game.Game; +import inf101.v18.rogue101.items.buff.BuffItem; +import inf101.v18.rogue101.items.weapon.BasicBow; +import inf101.v18.rogue101.items.weapon.BasicSword; +import inf101.v18.rogue101.items.weapon.FireStaff; +import inf101.v18.rogue101.items.weapon.MagicWeapon; +import inf101.v18.rogue101.items.weapon.MeleeWeapon; +import inf101.v18.rogue101.items.weapon.RangedWeapon; +import inf101.v18.rogue101.object.Item; +import inf101.v18.rogue101.state.AttackType; +import inf101.v18.rogue101.state.LifeStage; +import inf101.v18.rogue101.state.Occupation; +import inf101.v18.rogue101.state.Personality; +import inf101.v18.util.NPCHelper; import java.util.ArrayList; import java.util.Collections; import java.util.List; import java.util.Random; -public class Girl implements INonPlayer { +/** + * An enemy NPC + */ +public class Girl extends Enemy { + private final String name = randomName(); - private final Age age = Age.getRandom(); + private final LifeStage lifeStage = LifeStage.getRandom(); private final Personality personality = Personality.getRandom(); private final Occupation occupation = Occupation.getRandom(); - private int maxhp; - private int hp; + private int maxHp; private int attack; private int defence; private int visibility; private int damage; - private final Backpack backpack = new Backpack(); + private List damageDirections; private static final Random random = new Random(); private List> validItems; - private static final String[] namelist = {"Milk", "Salad", "Mikan", "Rima", "Suki", "Miki", "Hinata", "Haruna", "Asuna"}; + private static final String[] nameList = {"Milk", "Salad", "Mikan", "Rima", "Suki", "Miki", "Hinata", "Haruna", + "Asuna"}; + /** + * Instantiates a new Girl enemy NPC + */ public Girl() { setStats(); setValidItems(); } private void setStats() { - switch (age) { + // Set stats based on the NPC's age + switch (lifeStage) { case TODDLER: - maxhp = 30; + maxHp = 30; attack = 10; defence = 50; damage = 1 + random.nextInt(5); visibility = 1; break; case CHILD: - maxhp = 50; + maxHp = 50; attack = 20; defence = 40; damage = 2 + random.nextInt(5); visibility = 2; break; case TEEN: - maxhp = 70; + maxHp = 70; attack = 25; defence = 30; damage = 3 + random.nextInt(5); visibility = 3; break; case ADULT: - maxhp = 100; + maxHp = 100; attack = 30; defence = 20; damage = 4 + random.nextInt(5); visibility = 4; break; case ELDER: - maxhp = 70; + maxHp = 70; attack = 15; defence = 35; damage = 3 + random.nextInt(5); visibility = 2; break; } + // Improve stats based on occupation if (occupation == Occupation.KNIGHT) { attack += 10; //Knights are quite powerful. } else if (occupation == Occupation.MAGE) { - attack += 5; // Mages have lesser range than bowsmen, but more damage. + attack += 5; // Mages have lesser range than archers, but more damage. } - maxhp += (int)(random.nextGaussian() * 10); - hp = maxhp; - attack += (int)(random.nextGaussian() * 5); - defence += (int)(random.nextGaussian() * 5); + // Add random stat improvements + maxHp += (int) (random.nextGaussian() * 10); + hp = maxHp; + attack += (int) (random.nextGaussian() * 5); + defence += (int) (random.nextGaussian() * 5); } /** * Has a chance of giving a weapon to a girl. * - * @param lvl The current map level + * @param lvl The current map level */ public void giveWeapon(int lvl) { - if (random.nextInt(5) < lvl) { - switch (occupation) { - case KNIGHT: - backpack.add(new Sword()); - break; - case MAGE: - backpack.add(new Staff()); - break; - case BOWSMAN: - backpack.add(new Bow()); - } + // Randomly grant a weapon based on occupation. + // The chance of being granted a weapon increases for each level + if (random.nextInt(5) >= lvl) { + return; + } + switch (occupation) { + case KNIGHT: + backpack.add(new BasicSword()); + break; + case MAGE: + backpack.add(new FireStaff()); + break; + case ARCHER: + backpack.add(new BasicBow()); } } @@ -112,48 +131,70 @@ public class Girl implements INonPlayer { private void setValidItems() { validItems = new ArrayList<>(); switch (occupation) { - case BOWSMAN: - validItems.add(IRangedWeapon.class); + case ARCHER: + validItems.add(RangedWeapon.class); break; case MAGE: - validItems.add(IMagicWeapon.class); + validItems.add(MagicWeapon.class); break; case KNIGHT: - validItems.add(IMeleeWeapon.class); + validItems.add(MeleeWeapon.class); } - validItems.add(IBuffItem.class); + validItems.add(BuffItem.class); } /** * Picks a "random" name * - * @return A random name + * @return A random name */ private String randomName() { - return namelist[random.nextInt(namelist.length)] + "-chan"; + return nameList[random.nextInt(nameList.length)] + "-chan"; } @Override - public void doTurn(IGame game) { + public void doTurn(Game game) { + currentLocation = game.getLocation(); + List damageDirections = this.damageDirections; + this.damageDirections = null; + + // If this NPC has space, pick up any items on the ground if (backpack.hasSpace()) { - IItem item = NPC.pickUp(game, validItems); + Item item = NPCHelper.pickUp(game, validItems); if (item != null) { backpack.add(item); return; } - if (NPC.trackItem(game, validItems)) { + if (NPCHelper.trackItem(game, validItems)) { return; } } - if (personality == Personality.AFRAID && NPC.flee(game)) { - return; + // TODO: If attacked on the last turn, flee from the attacker if not within sight range + if ((personality == Personality.AFRAID || hp < 0.1 * maxHp)) { + // Flee from the nearest enemy + if (NPCHelper.flee(game)) { + return; + } + + // Flee in the direction this NPC was damaged from. This allows the NPC to flee from ranged attacks outside + // visible range. + if (damageDirections != null) { + for (GridDirection direction : damageDirections) { + if (game.canGo(direction)) { + game.move(direction); + return; + } + } + } } + // Attack if necessary if (willAttack(game) && attack(game)) { return; } + // Move a random direction if possible List possibleMoves = game.getPossibleMoves(); if (!possibleMoves.isEmpty()) { Collections.shuffle(possibleMoves); @@ -164,22 +205,22 @@ public class Girl implements INonPlayer { /** * Tries to attack with an attack specified by the Girl's occupation * - * @param game An IGame object - * @return True if an attack occurred. False otherwise + * @param game An IGame object + * @return True if an attack occurred. False otherwise */ - private boolean attack(IGame game) { + private boolean attack(Game game) { switch (occupation) { case KNIGHT: - if (NPC.tryAttack(game, 1, Attack.MELEE)) { + if (NPCHelper.tryAttack(game, 1, AttackType.MELEE)) { return true; } break; case MAGE: - if (NPC.tryAttack(game, getVision() / 2 + getVision() % 2 == 0 ? 0 : 1, Attack.MAGIC)) { + if (NPCHelper.tryAttack(game, getVision() / 2 + getVision() % 2 == 0 ? 0 : 1, AttackType.MAGIC)) { return true; } - case BOWSMAN: - if (NPC.tryAttack(game, getVision(), Attack.RANGED)) { + case ARCHER: + if (NPCHelper.tryAttack(game, getVision(), AttackType.RANGED)) { return true; } } @@ -189,23 +230,26 @@ public class Girl implements INonPlayer { /** * Checks if the current girl will try to attack. * - * @param game An IGame object - * @return True if the girl will attack. False otherwise + * @param game An IGame object + * @return True if the girl will attack. False otherwise */ - private boolean willAttack(IGame game) { + private boolean willAttack(Game game) { boolean attack = false; switch (personality) { case CALM: - if (hp < maxhp) { + // Attack if previously attacked + if (hp < maxHp) { attack = true; } break; case AFRAID: + // Attack if cornered if (game.getPossibleMoves().isEmpty()) { attack = true; } break; - case AGRESSIVE: + case AGGRESSIVE: + // Always attack attack = true; break; } @@ -228,13 +272,13 @@ public class Girl implements INonPlayer { } @Override - public int getDefence() { + public int getDefense() { return defence; } @Override public int getMaxHealth() { - return maxhp; + return maxHp; } @Override @@ -263,18 +307,12 @@ public class Girl implements INonPlayer { } @Override - public IItem getItem(Class type) { - for (IItem item : backpack.getContent()) { - if (type.isInstance(item)) { - return item; - } - } - return null; - } - - @Override - public int handleDamage(IGame game, IItem source, int amount) { - hp -= amount; + public int handleDamage(Game game, Item source, int amount) { + super.handleDamage(game, source, amount); + Location location = game.getMap().getLocation(source); + Location location2 = game.getMap().getLocation(this); + damageDirections = game.locationDirection(location, location2); return amount; } + } diff --git a/src/main/java/inf101/v18/rogue101/events/GameEvent.java b/src/main/java/inf101/v18/rogue101/events/GameEvent.java index b669866..44e0915 100644 --- a/src/main/java/inf101/v18/rogue101/events/GameEvent.java +++ b/src/main/java/inf101/v18/rogue101/events/GameEvent.java @@ -1,145 +1,97 @@ package inf101.v18.rogue101.events; -import inf101.v18.rogue101.game.IGame; -import inf101.v18.rogue101.objects.IItem; +import inf101.v18.rogue101.game.Game; +import inf101.v18.rogue101.object.Item; /** * Example implementation of events – could be used to have more complex * behaviour than just attack/defend/get damaged/pickup/drop. - * - * @author anya * - * @param - * Relevant extra data for this particular event + * @param Relevant extra data for this particular event + * @author anya */ public class GameEvent implements IEvent { - public static final String ATTACK_FAILURE = "attackFailure"; - public static final String ATTACK_SUCCESS = "attackSuccess"; - public static final String DEFEND_FAILURE = "defendFailure"; - public static final String DEFEND_SUCCESS = "defendSuccess"; - public static final String EATEN = "eaten"; - /** - * Create and send events for an attack. - *

    - * Both attacker and defender will receive appropriate events (ATTACK_* or - * DEFEND_*), depending on who “won”. The amount of damage is available through - * {@link #getData()}. - *

    - * Attacker will be sent the "damage" that was actually done (as returned by - * defender's event handler) - *

    - * Methods such as these could sensible be placed in IGame/Game. - * - * @param success - * True if attack succeeded (attacker "won") - * @param attacker - * @param defender - * @param damage - */ - public static void triggerAttack(boolean success, IItem attacker, IItem defender, int damage) { - if (success) { - Integer result = defender - .handleEvent(new GameEvent(DEFEND_FAILURE, null, attacker, defender, damage)); - if (result != null) - damage = result; - attacker.handleEvent(new GameEvent(ATTACK_SUCCESS, null, attacker, defender, damage)); - } else { - attacker.handleEvent(new GameEvent(ATTACK_FAILURE, null, attacker, defender, 0)); - defender.handleEvent(new GameEvent(DEFEND_SUCCESS, null, attacker, defender, 0)); - } - } + private final String name; + private final Item source; + private final Item target; + private T value; - private final String name; - private final IItem source; - private final IItem target; - private T value; + private final Game game; - private final IGame game; + /** + * Create a new game event + * + * @param name The name is used when checking which event this is / determine its + * “meaning” + * @param game The game, or null if unknown/not relevant + * @param source The item that caused the event, or null if + * unknown/not relevant + * @param target The item that receives the event, or null if + * unknown/not relevant + * @param value Arbitrary extra data + */ + public GameEvent(String name, Game game, Item source, Item target, T value) { + this.name = name; + this.game = game; + this.source = source; + this.target = target; + this.value = value; + } - /** - * Create a new game event - * - * @param name - * The name is used when checking which event this is / determine its - * “meaning” - * @param game - * The game, or null if unknown/not relevant - * @param source - * The item that caused the event, or null if - * unknown/not relevant - * @param target - * The item that receives the event, or null if - * unknown/not relevant - * @param value - * Arbitrary extra data - */ - public GameEvent(String name, IGame game, IItem source, IItem target, T value) { - this.name = name; - this.game = game; - this.source = source; - this.target = target; - this.value = value; - } + /** + * Create a new game event + * + * @param name The name is used when checking which event this is / determine its + * “meaning” + * @param source The item that caused the event, or null if + * unknown/not relevant + */ + public GameEvent(String name, Item source) { + this(name, null, source, null, null); + } - /** - * Create a new game event - * - * @param name - * The name is used when checking which event this is / determine its - * “meaning” - * @param source - * The item that caused the event, or null if - * unknown/not relevant - */ - public GameEvent(String name, IItem source) { - this(name, null, source, null, null); - } + /** + * Create a new game event + * + * @param name The name is used when checking which event this is / determine its + * “meaning” + * @param source The item that caused the event, or null if + * unknown/not relevant + * @param value Arbitrary extra data + */ + public GameEvent(String name, Item source, T value) { + this(name, null, source, null, value); + } - /** - * Create a new game event - * - * @param name - * The name is used when checking which event this is / determine its - * “meaning” - * @param source - * The item that caused the event, or null if - * unknown/not relevant - * @param value - * Arbitrary extra data - */ - public GameEvent(String name, IItem source, T value) { - this(name, null, source, null, value); - } + @Override + public T getData() { + return value; + } - @Override - public T getData() { - return value; - } + @Override + public String getEventName() { + return name; + } - @Override - public String getEventName() { - return name; - } + @Override + public Game getGame() { + return game; + } - @Override - public IGame getGame() { - return game; - } + @Override + public Item getSource() { + return source; + } - @Override - public IItem getSource() { - return source; - } + @Override + public Item getTarget() { + return target; + } - @Override - public IItem getTarget() { - return target; - } - - @Override - public void setData(T value) { - this.value = value; - } + @Override + public void setData(T value) { + this.value = value; + } } diff --git a/src/main/java/inf101/v18/rogue101/events/IEvent.java b/src/main/java/inf101/v18/rogue101/events/IEvent.java index 2edebe1..f698129 100644 --- a/src/main/java/inf101/v18/rogue101/events/IEvent.java +++ b/src/main/java/inf101/v18/rogue101/events/IEvent.java @@ -1,7 +1,7 @@ package inf101.v18.rogue101.events; -import inf101.v18.rogue101.game.IGame; -import inf101.v18.rogue101.objects.IItem; +import inf101.v18.rogue101.game.Game; +import inf101.v18.rogue101.object.Item; /** * An “event” is something that happens in the game, typically due to an actor @@ -19,54 +19,51 @@ import inf101.v18.rogue101.objects.IItem; * source/targets of the event (where relevant). *

    * This system is fairly simplistic, and you're not expected to make use of it. - * - * @author anya * - * @param - * Type of the extra data + * @param Type of the extra data + * @author anya */ public interface IEvent { - /** - * @return Extra data stored in this event - */ - T getData(); + /** + * @return Extra data stored in this event + */ + T getData(); - /** - * @return The name of this event - */ - String getEventName(); + /** + * @return The name of this event + */ + String getEventName(); - /** - * Not all events need to be connected to the game, but you can use this if you - * need it (e.g., for showing a message, or adding something to the map). - *

    - * The result might be null. - * - * @return The game associated with this event, or null. - */ - IGame getGame(); + /** + * Not all events need to be connected to the game, but you can use this if you + * need it (e.g., for showing a message, or adding something to the map). + *

    + * The result might be null. + * + * @return The game associated with this event, or null. + */ + Game getGame(); - /** - * The source is the item that “caused” the event - *

    - * Could be null if the source is unknown or not relevant. - * - * @return The source of this event - */ - IItem getSource(); + /** + * The source is the item that “caused” the event + *

    + * Could be null if the source is unknown or not relevant. + * + * @return The source of this event + */ + Item getSource(); - /** - * The target is the item that is affected by the event - *

    - * Could be null if the target is unknown or not relevant. - * - * @return The target of this event, or null - */ - IItem getTarget(); + /** + * The target is the item that is affected by the event + *

    + * Could be null if the target is unknown or not relevant. + * + * @return The target of this event, or null + */ + Item getTarget(); - /** - * @param value - * Extra data to store in this event - */ - void setData(T value); + /** + * @param value Extra data to store in this event + */ + void setData(T value); } diff --git a/src/main/java/inf101/v18/rogue101/examples/Carrot.java b/src/main/java/inf101/v18/rogue101/examples/Carrot.java index 86d6d9b..3e668e9 100644 --- a/src/main/java/inf101/v18/rogue101/examples/Carrot.java +++ b/src/main/java/inf101/v18/rogue101/examples/Carrot.java @@ -1,82 +1,82 @@ package inf101.v18.rogue101.examples; import inf101.v18.gfx.gfxmode.ITurtle; -import inf101.v18.rogue101.game.IGame; -import inf101.v18.rogue101.objects.IItem; +import inf101.v18.rogue101.game.Game; +import inf101.v18.rogue101.object.Item; import javafx.scene.paint.Color; -public class Carrot implements IItem { - private int hp = getMaxHealth(); +public class Carrot implements Item { + private int hp = getMaxHealth(); - public void doTurn() { - hp = Math.min(hp + 1, getMaxHealth()); - } + public void doTurn() { + hp = Math.min(hp + 1, getMaxHealth()); + } - @Override - public boolean draw(ITurtle painter, double w, double h) { - painter.save(); - painter.turn(75); - double size = ((double) hp + getMaxHealth()) / (2.0 * getMaxHealth()); - double carrotLength = size * h * .6; - double carrotWidth = size * h * .4; - painter.jump(-carrotLength / 6); - painter.shape().ellipse().width(carrotLength).height(carrotWidth).fillPaint(Color.ORANGE).fill(); - painter.jump(carrotLength / 2); - painter.setInk(Color.FORESTGREEN); - for (int i = -1; i < 2; i++) { - painter.save(); - painter.turn(20 * i); - painter.draw(carrotLength / 3); - painter.restore(); - } - painter.restore(); - return true; - } + @Override + public boolean draw(ITurtle painter, double w, double h) { + painter.save(); + painter.turn(75); + double size = ((double) hp + getMaxHealth()) / (2.0 * getMaxHealth()); + double carrotLength = size * h * .6; + double carrotWidth = size * h * .4; + painter.jump(-carrotLength / 6); + painter.shape().ellipse().width(carrotLength).height(carrotWidth).fillPaint(Color.ORANGE).fill(); + painter.jump(carrotLength / 2); + painter.setInk(Color.FORESTGREEN); + for (int i = -1; i < 2; i++) { + painter.save(); + painter.turn(20 * i); + painter.draw(carrotLength / 3); + painter.restore(); + } + painter.restore(); + return true; + } - @Override - public int getCurrentHealth() { - return hp; - } + @Override + public int getCurrentHealth() { + return hp; + } - @Override - public int getDefence() { - return 0; - } + @Override + public int getDefense() { + return 0; + } - @Override - public double getHealthStatus() { - return getCurrentHealth() / getMaxHealth(); - } + @Override + public double getHealthStatus() { + return getCurrentHealth() / getMaxHealth(); + } - @Override - public int getMaxHealth() { - return 23; - } + @Override + public int getMaxHealth() { + return 23; + } - @Override - public String getName() { - return "carrot"; - } + @Override + public String getName() { + return "carrot"; + } - @Override - public int getSize() { - return 2; - } + @Override + public int getSize() { + return 2; + } - @Override - public String getSymbol() { - return "C"; - } + @Override + public String getSymbol() { + return "C"; + } - @Override - public int handleDamage(IGame game, IItem source, int amount) { - hp -= amount; + @Override + public int handleDamage(Game game, Item source, int amount) { + hp -= amount; - if (hp < 0) { - // we're all eaten! - hp = 0; - } - return amount; - } + if (hp < 0) { + // we're all eaten! + hp = 0; + } + return amount; + } } diff --git a/src/main/java/inf101/v18/rogue101/examples/ExampleItem.java b/src/main/java/inf101/v18/rogue101/examples/ExampleItem.java deleted file mode 100644 index dca6607..0000000 --- a/src/main/java/inf101/v18/rogue101/examples/ExampleItem.java +++ /dev/null @@ -1,51 +0,0 @@ -package inf101.v18.rogue101.examples; - -import inf101.v18.gfx.gfxmode.ITurtle; -import inf101.v18.rogue101.game.IGame; -import inf101.v18.rogue101.objects.IItem; - -public class ExampleItem implements IItem { - private int hp = getMaxHealth(); - - @Override - public boolean draw(ITurtle painter, double w, double h) { - return false; - } - - @Override - public int getCurrentHealth() { - return hp; - } - - @Override - public int getDefence() { - return 0; - } - - @Override - public int getMaxHealth() { - return 1; - } - - @Override - public String getName() { - return "strange model of an item"; - } - - @Override - public int getSize() { - return 1; - } - - @Override - public String getSymbol() { - return "X"; - } - - @Override - public int handleDamage(IGame game, IItem source, int amount) { - hp -= amount; - return amount; - } - -} diff --git a/src/main/java/inf101/v18/rogue101/examples/Rabbit.java b/src/main/java/inf101/v18/rogue101/examples/Rabbit.java index d52cbb4..668d126 100644 --- a/src/main/java/inf101/v18/rogue101/examples/Rabbit.java +++ b/src/main/java/inf101/v18/rogue101/examples/Rabbit.java @@ -1,122 +1,123 @@ package inf101.v18.rogue101.examples; +import inf101.v18.gfx.gfxmode.ITurtle; +import inf101.v18.grid.GridDirection; +import inf101.v18.rogue101.game.Game; +import inf101.v18.rogue101.object.Item; +import inf101.v18.rogue101.object.NonPlayerCharacter; +import inf101.v18.rogue101.state.AttackType; +import inf101.v18.util.NPCHelper; + import java.util.ArrayList; import java.util.Collections; import java.util.List; -import inf101.v18.gfx.gfxmode.ITurtle; -import inf101.v18.grid.GridDirection; -import inf101.v18.rogue101.game.IGame; -import inf101.v18.rogue101.objects.IItem; -import inf101.v18.rogue101.objects.INonPlayer; -import inf101.v18.rogue101.shared.NPC; -import inf101.v18.rogue101.states.Attack; +public class Rabbit implements NonPlayerCharacter { + private int food = 0; + private int hp = getMaxHealth(); + private static final List> validItems = new ArrayList<>(); -public class Rabbit implements INonPlayer { - private int food = 0; - private int hp = getMaxHealth(); - private static final List> validItems = new ArrayList<>(); - static { - validItems.add(Carrot.class); - } + static { + validItems.add(Carrot.class); + } - @Override - public void doTurn(IGame game) { - if (food == 0) { - hp--; - } else { - food--; - } - if (hp < 1) { - return; - } + @Override + public void doTurn(Game game) { + if (food == 0) { + hp--; + } else { + food--; + } + if (hp < 1) { + return; + } - if (NPC.tryAttack(game, 1, Attack.MELEE)) { - return; - } + if (NPCHelper.tryAttack(game, 1, AttackType.MELEE)) { + return; + } - for (IItem item : game.getLocalItems()) { - if (item instanceof Carrot) { - System.out.println("found carrot!"); - int eaten = item.handleDamage(game, this, getDamage()); - if (eaten > 0) { - System.out.println("ate carrot worth " + eaten + "!"); - food += eaten; - game.displayMessage("You hear a faint crunching (" + getName() + " eats " + item.getArticle() + " " + item.getName() + ")"); - return; - } - } - } + for (Item item : game.getLocalItems()) { + if (item instanceof Carrot) { + System.out.println("found carrot!"); + int eaten = item.handleDamage(game, this, getDamage()); + if (eaten > 0) { + System.out.println("ate carrot worth " + eaten + "!"); + food += eaten; + game.displayMessage("You hear a faint crunching (" + getName() + " eats " + item.getArticle() + " " + item.getName() + ")"); + return; + } + } + } - if (NPC.trackItem(game, validItems)) { - return; - } + if (NPCHelper.trackItem(game, validItems)) { + return; + } - List possibleMoves = game.getPossibleMoves(); - if (!possibleMoves.isEmpty()) { - Collections.shuffle(possibleMoves); - game.move(possibleMoves.get(0)); - } - } + List possibleMoves = game.getPossibleMoves(); + if (!possibleMoves.isEmpty()) { + Collections.shuffle(possibleMoves); + game.move(possibleMoves.get(0)); + } + } - @Override - public boolean draw(ITurtle painter, double w, double h) { - return false; - } + @Override + public boolean draw(ITurtle painter, double w, double h) { + return false; + } - @Override - public int getAttack() { - return 10; - } + @Override + public int getAttack() { + return 10; + } - @Override - public int getCurrentHealth() { - return hp; - } + @Override + public int getCurrentHealth() { + return hp; + } - @Override - public int getDamage() { - return 5; - } + @Override + public int getDamage() { + return 5; + } - @Override - public int getDefence() { - return 10; - } + @Override + public int getDefense() { + return 10; + } - @Override - public int getMaxHealth() { - return 30; - } + @Override + public int getMaxHealth() { + return 30; + } - @Override - public String getName() { - return "Rabbit"; - } + @Override + public String getName() { + return "Rabbit"; + } - @Override - public int getSize() { - return 4; - } + @Override + public int getSize() { + return 4; + } - @Override - public int getVision() { - return 4; - } + @Override + public int getVision() { + return 4; + } - @Override - public IItem getItem(Class type) { - return null; - } + @Override + public Item getItem(Class type) { + return null; + } - @Override - public String getSymbol() { - return hp > 0 ? "R" : "¤"; - } + @Override + public String getSymbol() { + return hp > 0 ? "R" : "¤"; + } - @Override - public int handleDamage(IGame game, IItem source, int amount) { - hp -= amount; - return amount; - } + @Override + public int handleDamage(Game game, Item source, int amount) { + hp -= amount; + return amount; + } } diff --git a/src/main/java/inf101/v18/rogue101/game/Game.java b/src/main/java/inf101/v18/rogue101/game/Game.java index 644af07..7bbdb9b 100644 --- a/src/main/java/inf101/v18/rogue101/game/Game.java +++ b/src/main/java/inf101/v18/rogue101/game/Game.java @@ -1,816 +1,325 @@ package inf101.v18.rogue101.game; -import java.util.ArrayList; -import java.util.Collections; -import java.util.HashMap; -import java.util.Iterator; -import java.util.List; -import java.util.Map; -import java.util.Random; -import java.util.function.Supplier; - -import inf101.v18.gfx.Screen; import inf101.v18.gfx.gfxmode.ITurtle; -import inf101.v18.gfx.gfxmode.TurtlePainter; import inf101.v18.gfx.textmode.Printer; import inf101.v18.grid.GridDirection; -import inf101.v18.grid.IGrid; -import inf101.v18.grid.ILocation; -import inf101.v18.rogue101.Main; -import inf101.v18.rogue101.enemies.Boss; -import inf101.v18.rogue101.enemies.Girl; -import inf101.v18.rogue101.examples.Carrot; -import inf101.v18.rogue101.items.Manga; -import inf101.v18.rogue101.items.*; -import inf101.v18.rogue101.examples.Rabbit; -import inf101.v18.rogue101.map.GameMap; -import inf101.v18.rogue101.map.IGameMap; -import inf101.v18.rogue101.map.IMapView; -import inf101.v18.rogue101.map.MapReader; -import inf101.v18.rogue101.objects.*; -import inf101.v18.rogue101.shared.NPC; -import inf101.v18.rogue101.states.Attack; -import javafx.scene.canvas.GraphicsContext; -import javafx.scene.input.KeyCode; -import javafx.scene.paint.Color; +import inf101.v18.grid.Location; +import inf101.v18.rogue101.map.MapView; +import inf101.v18.rogue101.object.Actor; +import inf101.v18.rogue101.object.Item; +import inf101.v18.rogue101.object.NonPlayerCharacter; +import inf101.v18.rogue101.object.PlayerCharacter; +import inf101.v18.rogue101.state.AttackType; -public class Game implements IGame { - /** - * All the IActors that have things left to do this turn - */ - private List actors = Collections.synchronizedList(new ArrayList<>()); - /** - * For fancy solution to factory problem - */ - private Map> itemFactories = new HashMap<>(); - /** - * Useful random generator - */ - private Random random = new Random(); +import java.util.List; +import java.util.Random; +/** + * Game interface + *

    + * The game has a map and a current {@link Actor} (the player or non-player + * whose turn it currently is). The game also knows the current location of the + * actor. Most methods that deal with the map will use this current location – + * they are meant to be used by the current actor for exploring or interacting + * with its surroundings. + *

    + * In other words, you should avoid calling most of these methods if you're not + * the current actor. You know you're the current actor when you're inside your + * {@link PlayerCharacter#keyPressed()} or {@link NonPlayerCharacter#doTurn()} method. + * + * @author anya + */ +public interface Game { /** - * Saves the last three messages - */ - private List lastMessages = new ArrayList<>(); - - /** - * The game map. {@link IGameMap} gives us a few more details than - * {@link IMapView} (write access to item lists); the game needs this but - * individual items don't. - */ - private IGameMap map; - private List maps = new ArrayList<>(); - private int currentLVL = 0; - private IActor currentActor; - private ILocation currentLocation; - private int movePoints = 0; - private final ITurtle painter; - private final Printer printer; - private int numPlayers = 0; - private boolean won = false; - - public Game(Screen screen, ITurtle painter, Printer printer) { - this.painter = painter; - this.printer = printer; - - addFactory(); - - // NOTE: in a more realistic situation, we will have multiple levels (one map - // per level), and (at least for a Roguelike game) the levels should be - // generated - // - // inputGrid will be filled with single-character strings indicating what (if - // anything) - // should be placed at that map square - loadMap("level1.txt", 1); - loadMap("level2.txt", 2); - loadMap("level3.txt", 3); - loadMap("level4.txt", 4); - loadMap("level5.txt", 5); - map = maps.get(0); - map.add(map.getLocation( 3, 3), new Player()); //We must be sure there is never more than one player - - // Prints some helpful information. - String[] info = {"Controls:", "WASD or arrow keys for movement", "E to pick up an item", "Q to drop an item", "1-0 to choose an item (10=0)", "N to change name", "ENTER to confirm", "R to use a ranged attack", "F to use a magic attack", "C to use a consumable"}; - for (int i = 0; i < info.length; i++) { - this.printer.printAt(Main.COLUMN_RIGHTSIDE_START, 1 + i, info[i]); - } - } - - public Game(String mapString) { - printer = new Printer(1280, 720); - painter = new TurtlePainter(1280, 720); - - addFactory(); - - IGrid inputGrid = MapReader.readString(mapString); - this.map = new GameMap(inputGrid.getArea()); - for (ILocation loc : inputGrid.locations()) { - IItem item = createItem(inputGrid.get(loc)); - if (item != null) { - map.add(loc, item); - } - } - } - - @Override - public void addItem(IItem item) { - map.add(currentLocation, item); - } - - @Override - public void addItem(String sym) { - IItem item = createItem(sym); - if (item != null) - map.add(currentLocation, item); - } - - /** - * Goes up one floor if possible. - */ - public void goUp() { - if (currentLVL + 1 < maps.size()) { - map = maps.get(++currentLVL); - if (!map.has(currentLocation, currentActor)) { - map.add(currentLocation, currentActor); - } - actors = new ArrayList<>(); - displayMessage("You went up the stairs"); - } - } - - /** - * Goes down one floor if possible. - */ - public void goDown() { - if (currentLVL > 0) { - map = maps.get(--currentLVL); - if (!map.has(currentLocation, currentActor)) { - map.add(currentLocation, currentActor); - } - actors = new ArrayList<>(); - displayMessage("You went down the stairs"); - } - } - - /** - * Calculates the attack of an IActor based on equipped items. - * - * @return The attack - */ - private int getAttack(Attack type) { - int attack = currentActor.getAttack() + random.nextInt(20) + 1; - IWeapon weapon = NPC.getWeapon(type, currentActor); - if (weapon != null) { - attack += weapon.getWeaponAttack(); - } - return attack; - } - - /** - * Gets the defence of the current target. - * - * @param target The target to evaluate - * @return The defence of the target - */ - private int getDefence(IItem target) { - int defence = target.getDefence() + 10; - IActor actor = (IActor) target; - IBuffItem item = (IBuffItem) actor.getItem(IBuffItem.class); - if (item != null) { - defence += item.getBuffDefence(); - } - return defence; - } - - /** - * Gets the damage done against the current target. - * - * @param target The target to evaluate. - * @return The damage done to the target. - */ - private int getDamage(IItem target, Attack type) { - int damage = currentActor.getDamage(); - IWeapon weapon = NPC.getWeapon(type, currentActor); - if (weapon != null) { - damage += weapon.getWeaponDamage(); - } - IBuffItem buff = (IBuffItem)currentActor.getItem(IBuffItem.class); - if (buff != null) { - damage += buff.getBuffDamage(); - } - IBuffItem item = (IBuffItem)((IActor)target).getItem(IBuffItem.class); - if (item != null) { - damage -= item.getBuffDamageReduction(); - } - return damage; - } - - @Override - public ILocation attack(GridDirection dir, IItem target) { - ILocation loc = currentLocation.go(dir); - if (!map.has(loc, target)) { - throw new IllegalMoveException("Target isn't there!"); - } - IWeapon weapon = (IWeapon) currentActor.getItem(IMeleeWeapon.class); - if (weapon != null) { - NPC.playSound(weapon.getSound()); - } else { - NPC.playSound("audio/Realistic_Punch-Mark_DiAngelo-1609462330.wav"); - } - if (getAttack(Attack.MELEE) >= getDefence(target)) { - int actualDamage = target.handleDamage(this, target, getDamage(target, Attack.MELEE)); - if (currentActor != null) { - formatMessage("%s hits %s for %d damage", currentActor.getName(), target.getName(), actualDamage); - } - } else { - formatMessage("%s tried to hit %s, but missed", currentActor.getName(), target.getName()); - } - - map.clean(loc); - - if (target.isDestroyed() && currentLocation != null) { - return move(dir); - } else { - movePoints--; - return currentLocation; - } - } - - @Override - public ILocation rangedAttack(GridDirection dir, IItem target, Attack type) { - ILocation loc = currentLocation; - IWeapon weapon = null; - switch (type) { - case MAGIC: - weapon = (IWeapon) currentActor.getItem(IMagicWeapon.class); - break; - case RANGED: - weapon = (IWeapon) currentActor.getItem(IRangedWeapon.class); - } - if (weapon != null) { - NPC.playSound(weapon.getSound()); - } else { - NPC.playSound("audio/Snow Ball Throw And Splat-SoundBible.com-992042947.wav"); - } - if (getAttack(type) >= getDefence(target)) { - int damage = getDamage(target, type) / loc.gridDistanceTo(map.getLocation(target)); - int actualDamage = target.handleDamage(this, target, damage); - formatMessage("%s hits %s for %d damage", currentActor.getName(), target.getName(), actualDamage); - } else { - formatMessage("%s tried to hit %s, but missed", currentActor.getName(), target.getName()); - } - map.clean(map.getLocation(target)); - if (target.isDestroyed() && map.has(currentLocation.go(dir), target)) { - return move(dir); - } else { - return currentLocation; - } - } - - /** - * Begin a new game turn, or continue to the previous turn - * - * @return True if the game should wait for more user input - */ - public boolean doTurn() { - do { - if (actors.isEmpty()) { - // System.err.println("new turn!"); - - // no one in the queue, we're starting a new turn! - // first collect all the actors: - beginTurn(); - } - - /*if (random.nextInt(100) < 20) { - ILocation loc = map.getLocation(random.nextInt(map.getWidth()), random.nextInt(map.getHeight())); - if (!map.hasActors(loc) && !map.hasItems(loc) && !map.hasWall(loc)) { - map.add(loc, new Carrot()); - } - }*/ //We don't want this in the actual game. - - // process actors one by one; for the IPlayer, we return and wait for keypresses - // Possible for INonPlayer, we could also return early (returning - // *false*), and then insert a little timer delay between each non-player move - // (the timer - // is already set up in Main) - if (numPlayers == 0 && !won) { - kill(); - } - while (!actors.isEmpty()) { - // get the next player or non-player in the queue - currentActor = actors.remove(0); - if (currentActor == null) { - return false; //TODO: Find out why a girl suddenly becomes null (only after map change, not caught in beginTurn, happens randomly) - } - if (currentActor.isDestroyed()) // skip if it's dead - continue; - currentLocation = map.getLocation(currentActor); - if (currentLocation == null) { - displayDebug("doTurn(): Whoops! Actor has disappeared from the map: " + currentActor); - } - movePoints = 1; // everyone gets to do one thing - - if (currentActor instanceof INonPlayer) { - // computer-controlled players do their stuff right away - ((INonPlayer) currentActor).doTurn(this); - // remove any dead items from current location - map.clean(currentLocation); - return false; - } else if (currentActor instanceof IPlayer) { - if (!currentActor.isDestroyed()) { - // For the human player, we need to wait for input, so we just return. - // Further keypresses will cause keyPressed() to be called, and once the human - // makes a move, it'll lose its movement point and doTurn() will be called again - // - // NOTE: currentActor and currentLocation are set to the IPlayer (above), - // so the game remembers who the player is whenever new keypresses occur. This - // is also how e.g., getLocalItems() work – the game always keeps track of - // whose turn it is. - return true; - } - } else { - displayDebug("doTurn(): Hmm, this is a very strange actor: " + currentActor); - } - } - } while (numPlayers > 0); // we can safely repeat if we have players, since we'll return (and break out of - // the loop) once we hit the player - return true; - } - - /** - * Player is dead. It needs to be properly killed off. - */ - private void kill() { - map.remove(currentLocation, currentActor); - currentActor = null; - currentLocation = null; - actors = new ArrayList<>(); - loadMap("gameover.txt"); - NPC.playSound("audio/Dying-SoundBible.com-1255481835.wav"); - } - - public void win() { //Trigger when the boss dies - map.remove(currentLocation, currentActor); - currentActor = null; - currentLocation = null; - actors = new ArrayList<>(); - loadMap("victory.txt"); - map.draw(painter, printer); - NPC.playSound("audio/1_person_cheering-Jett_Rifkin-1851518140.wav"); - won = true; - } - - /** - * Loads a map with the desired name + * Add an item to the current location + *

    + * If the item is an actor, it won't move until the next turn. * - * @param mapName Name of map, including extension. + * @param item */ - private void loadMap(String mapName) { - IGrid inputGrid = MapReader.readFile("maps/" + mapName); - if (inputGrid == null) { - System.err.println("Map not found – falling back to builtin map"); - inputGrid = MapReader.readString(Main.BUILTIN_MAP); - } - IGameMap map = new GameMap(inputGrid.getArea()); - for (ILocation loc : inputGrid.locations()) { - IItem item = createItem(inputGrid.get(loc)); - if (item != null) { - map.add(loc, item); - } - } - this.map = map; - } + void addItem(Item item); - private void loadMap(String mapName, int lvl) { - IGrid inputGrid = MapReader.readFile("maps/" + mapName); - if (inputGrid == null) { - System.err.println("Map not found – falling back to builtin map"); - inputGrid = MapReader.readString(Main.BUILTIN_MAP); - } - IGameMap map = new GameMap(inputGrid.getArea()); - for (ILocation loc : inputGrid.locations()) { - IItem item = createItem(inputGrid.get(loc)); - if (item instanceof Chest) { - ((Chest) item).fill(lvl); - } else if (item instanceof Girl) { - ((Girl) item).giveWeapon(lvl); - } - if (item != null) { - map.add(loc, item); - } - } - maps.add(map); - } + /** + * Add a new item (identified by symbol) to the current location + *

    + * If the item is an actor, it won't move until the next turn. + * + * @param sym + */ + void addItem(String sym); - /** - * Go through the map and collect all the actors. - */ - private void beginTurn() { - numPlayers = 0; - // this extra fancy iteration over each map location runs *in parallel* on - // multicore systems! - // that makes some things more tricky, hence the "synchronized" block and - // "Collections.synchronizedList()" in the initialization of "actors". - // NOTE: If you want to modify this yourself, it might be a good idea to replace - // "parallelStream()" by "stream()", because weird things can happen when many - // things happen - // at the same time! (or do INF214 or DAT103 to learn about locks and threading) - map.getArea().parallelStream().forEach((loc) -> { // will do this for each location in map - List list = map.getAllModifiable(loc); // all items at loc - Iterator li = list.iterator(); // manual iterator lets us remove() items - while (li.hasNext()) { // this is what "for(IItem item : list)" looks like on the inside - IItem item = li.next(); - if (item.getCurrentHealth() < 0) { - // normally, we expect these things to be removed when they are destroyed, so - // this shouldn't happen - synchronized (this) { - formatDebug("beginTurn(): found and removed leftover destroyed item %s '%s' at %s%n", - item.getName(), item.getSymbol(), loc); - } - li.remove(); - map.remove(loc, item); // need to do this too, to update item map - } else if (item instanceof IPlayer) { - actors.add(0, (IActor) item); // we let the human player go first - synchronized (this) { - numPlayers++; - } - } else if (item instanceof IActor) { - actors.add((IActor) item); // add other actors to the end of the list - } else if (item instanceof Carrot) { - ((Carrot) item).doTurn(); - } - } - }); - } + /** + * Perform an attack on the target. + *

    + * Will use your {@link Actor#getAttack()} against the target's + * {@link Item#getDefense()}, and then possiblyu find the damage you're dealing + * with {@link Actor#getDamage()} and inform the target using + * {@link Item#handleDamage(Game, Item, int)} + * + * @param dir Direction + * @param target A target item, which should be in the neighbouring square in the + * given direction + * @return Your new location if the attack resulted in you moving + */ + Location attack(GridDirection dir, Item target); - @Override - public boolean canGo(GridDirection dir) { - return map.canGo(currentLocation, dir); - } + /** + * @param dir + * @return True if it's possible to move in the given direction + */ + boolean canGo(GridDirection dir); - private void addFactory() { - itemFactories.put("#", Wall::new); - itemFactories.put("@", Player::new); - itemFactories.put("C", Carrot::new); - itemFactories.put("R", Rabbit::new); - itemFactories.put("M", Manga::new); - itemFactories.put("G", Girl::new); - itemFactories.put(".", Dust::new); - itemFactories.put("S", Sword::new); - itemFactories.put("c", Chest::new); - itemFactories.put("B", Boss::new); - itemFactories.put("s", Staff::new); - itemFactories.put("b", Bow::new); - itemFactories.put("H", HealthPotion::new); - itemFactories.put("=", FakeWall::new); - itemFactories.put("<", StairsUp::new); - itemFactories.put(">", StairsDown::new); - itemFactories.put("m", Binoculars::new); - itemFactories.put("f", Shield::new); - } + /** + * Create a new item based on a text symbol + *

    + * The item won't be added to the map unless you call {@link #addItem(Item)}. + * + * @param symbol + * @return The new item + */ + Item createItem(String symbol); - @Override - public IItem createItem(String sym) { - if (" ".equals(sym)) { - return null; - }// alternative/advanced method - Supplier factory = itemFactories.get(sym); - if (factory != null) { - return factory.get(); - } else { - System.err.println("createItem: Don't know how to create a '" + sym + "'"); - return null; - } - } + /** + * Displays a message in the debug area on the screen (bottom line) + * + * @param s A message + */ + void displayDebug(String s); - @Override - public void displayDebug(String s) { - printer.clearLine(Main.LINE_DEBUG); - printer.printAt(1, Main.LINE_DEBUG, s, Color.DARKRED); - System.err.println(s); - } + /** + * Displays a message in the message area on the screen (below the map and the + * status line) + * + * @param s A message + */ + void displayMessage(String s); - @Override - public void displayMessage(String s) { - if (lastMessages.size() >= 3) { - lastMessages.remove(2); - } - lastMessages.add(0, s); - printer.clearLine(Main.LINE_MSG1); - printer.clearLine(Main.LINE_MSG2); - printer.clearLine(Main.LINE_MSG3); + /** + * Displays a status message in the status area on the screen (right below the + * map) + * + * @param s A message + */ + void displayStatus(String s); - int maxLen = 80; //The maximum length of a message to not overflow. - boolean secondLineWritten = false; - boolean thirdLineWritten = false; + /** + * Displays a message in the message area on the screen (below the map and the + * status line) + * + * @param s A message + * @see String#format(String, Object...) + */ + void formatDebug(String s, Object... args); - //Makes long messages overflow to the next line. - if (lastMessages.size() > 0) { - String message = lastMessages.get(0); - if (message.length() > 2 * maxLen) { - printer.printAt(1, Main.LINE_MSG1, message.substring(0, maxLen)); - printer.printAt(1, Main.LINE_MSG2, message.substring(maxLen, 2 * maxLen)); - printer.printAt(1, Main.LINE_MSG3, message.substring(2 * maxLen)); - secondLineWritten = thirdLineWritten = true; - } else if (message.length() > maxLen) { - printer.printAt(1, Main.LINE_MSG1, message.substring(0, maxLen)); - printer.printAt(1, Main.LINE_MSG2, message.substring(maxLen)); - secondLineWritten = true; - } else { - printer.printAt(1, Main.LINE_MSG1, message); - } - } - if (lastMessages.size() > 1 && !secondLineWritten) { - String message = lastMessages.get(1); - if (message.length() > maxLen) { - printer.printAt(1, Main.LINE_MSG2, message.substring(0, maxLen)); - printer.printAt(1, Main.LINE_MSG3, message.substring(maxLen)); - thirdLineWritten = true; - } else { - printer.printAt(1, Main.LINE_MSG2, message); - } - } - if (lastMessages.size() > 2 && !thirdLineWritten) { - printer.printAt(1, Main.LINE_MSG3, lastMessages.get(2)); - } - //System.out.println("Message: «" + s + "»"); - } + /** + * Displays a formatted message in the message area on the screen (below the map + * and the status line) + * + * @param s A message + * @see String#format(String, Object...) + */ + void formatMessage(String s, Object... args); - @Override - public void displayStatus(String s) { - printer.clearLine(Main.LINE_STATUS); - printer.printAt(1, Main.LINE_STATUS, s); - //System.out.println("Status: «" + s + "»"); - } + /** + * Displays a formatted status message in the status area on the screen (right + * below the map) + * + * @param s A message + * @see String#format(String, Object...) + */ + void formatStatus(String s, Object... args); - public void draw() { - if (numPlayers == 0) { - map.draw(painter, printer); - } else { - ((GameMap) map).drawVisible(painter, printer); - } - } + /** + * Pick up an item + *

    + * This should be used by IActors who want to pick up an item and carry it. The + * item will be returned if picking it succeeded (the actor might also + * make a mistake and pick up the wrong item!). + * + * @param item An item, should be in the current map location + * @return The item that was picked up (normally item), or + * null if it failed + */ + Item pickUp(Item item); - @Override - public boolean drop(IItem item) { - if (item != null) { - map.add(currentLocation, item); - return true; - } else - return false; - } + /** + * Drop an item + *

    + * This should be used by IActors who are carrying an item and want to put it on + * the ground. Check the return value to see if it succeeded. + * + * @param item An item, should be carried by the current actor and not already be + * on the map + * @return True if the item was placed on the map, false means you're still + * holding it + */ + boolean drop(Item item); - @Override - public boolean dropAt(ILocation loc, IItem item) { - if (item != null) { - map.add(loc, item); - return true; - } else - return false; - } + /** + * Does the same as drop, but for a specified location. + * + * @param loc The location to drop the location + * @param item The item to drop + * @return True if the item was dropped. False otherwise + */ + boolean dropAt(Location loc, Item item); - @Override - public void formatDebug(String s, Object... args) { - displayDebug(String.format(s, args)); - } + /** + * Clear the unused graphics area (you can fill it with whatever you want!) + */ + void clearFreeGraphicsArea(); - @Override - public void formatMessage(String s, Object... args) { - displayMessage(String.format(s, args)); - } + /** + * Clear the unused text area (you can fill it with whatever you want!) + */ + void clearFreeTextArea(); - @Override - public void formatStatus(String s, Object... args) { - displayStatus(String.format(s, args)); - } + /** + * Get the bounds of the free graphics area. + *

    + * You can fill this with whatever you want, using {@link #getPainter()} and + * {@link #clearFreeGraphicsArea()}. + * + * @return Array of coordinates; ([0],[1]) are the top-left corner, and + * ([2],[3]) are the bottom-right corner (exclusive). + */ + double[] getFreeGraphicsAreaBounds(); - @Override - public int getHeight() { - return map.getHeight(); - } + /** + * Get the bounds of the free text area. + *

    + * You can fill this with whatever you want, using {@link #getPrinter()} and + * {@link #clearFreeTextArea()}. + *

    + * You'll probably want to use something like: + * + *

    +     * int[] bounds = getFreeTextArea();
    +     * int x = bounds[0];
    +     * int y = bounds[1];
    +     * game.getPrinter().printAt(x, y++, "Hello");
    +     * game.getPrinter().printAt(x, y++, "Do you have any carrot cake?", Color.ORANGE);
    +     * 
    + * + * @return Array of column/line numbers; ([0],[1]) is the top-left corner, and + * ([2],[3]) is the bottom-right corner (inclusive). + */ + int[] getFreeTextAreaBounds(); - @Override - public List getLocalItems() { - return map.getItems(currentLocation); - } + /** + * See {@link #getFreeGraphicsAreaBounds()}, {@link #clearFreeGraphicsArea()}. + * + * @return A Turtle, for painting graphics + */ + ITurtle getPainter(); - @Override - public ILocation getLocation() { - return currentLocation; - } + /** + * See {@link #getFreeTextAreaBounds()}, {@link #clearFreeTextArea()}. + * + * @return A printer, for printing text + */ + Printer getPrinter(); - @Override - public ILocation getLocation(GridDirection dir) { - if (currentLocation.canGo(dir)) - return currentLocation.go(dir); - else - return null; - } + /** + * @return The height of the map + */ + int getHeight(); - /** - * Return the game map. {@link IGameMap} gives us a few more details than - * {@link IMapView} (write access to item lists); the game needs this but - * individual items don't. - */ - @Override - public IMapView getMap() { - return map; - } + /** + * @return A list of the non-actor items at the current map location + */ + List getLocalItems(); - @Override - public List getPossibleMoves() { - List moves = new ArrayList<>(); - for (GridDirection dir : GridDirection.FOUR_DIRECTIONS) { - if (canGo(dir)) { - moves.add(dir); - } - } - return moves; - } + /** + * Get the current actor's location. + *

    + * You should only call this from an IActor that is currently doing its move. + * + * @return Location of the current actor + */ + Location getLocation(); - @Override - public List getVisible() { - List neighbours = map.getNeighbourhood(currentLocation, currentActor.getVision()); - List invalid = new ArrayList<>(); - for (ILocation neighbour : neighbours) { - for (ILocation tile : currentLocation.gridLineTo(neighbour)) { - if (map.hasWall(tile)) { - invalid.add(neighbour); - break; - } - } - } - neighbours.removeAll(invalid); - return neighbours; - } + /** + * Get the current actor + *

    + * You can check if it's your move by doing game.getActor()==this. + * + * @return The current actor (i.e., the (IPlayer/INonPlayer) player who's turn it currently is) + */ + Actor getActor(); - @Override - public int getWidth() { - return map.getWidth(); - } + /** + * Get the neighbouring map location in direction dir + *

    + * Same as getLocation().go(dir) + * + * @param dir A direction + * @return A location, or null if the location would be outside the + * map + */ + Location getLocation(GridDirection dir); - public boolean keyPressed(KeyCode code) { - // only an IPlayer/human can handle keypresses, and only if it's the human's - // turn - return !(currentActor instanceof IPlayer) || !((IPlayer) currentActor).keyPressed(this, code); - } + /** + * @return The map + */ + MapView getMap(); - @Override - public ILocation move(GridDirection dir) { - if (movePoints < 1) - throw new IllegalMoveException("You're out of moves!"); - ILocation newLoc = map.go(currentLocation, dir); - map.remove(currentLocation, currentActor); - map.add(newLoc, currentActor); - currentLocation = newLoc; - movePoints--; - return currentLocation; - } + /** + * @return A list of directions we can move in, for use with + * {@link #move(GridDirection)} + */ + List getPossibleMoves(); - @Override - public IItem pickUp(IItem item) { - if (item != null && map.has(currentLocation, item) && !(item instanceof IStatic)) { - if (item instanceof IActor) { - if (item.getCurrentHealth() / item.getMaxHealth() < 3) { - map.remove(currentLocation, item); - return item; - } else { - return null; - } - } else if (currentActor.getAttack() > item.getDefence()) { - map.remove(currentLocation, item); - return item; - } else { - return null; - } - } else { - return null; - } - } + /** + * Get a list of all locations that are visible from the current location. + *

    + * The location list is sorted so that nearby locations come earlier in the + * list. E.g., if l = getVisible() and i < jthen + * getLocation().gridDistanceTo(l.get(i)) < getLocation().gridDistanceTo(l.get(j)) + * + * @return A list of grid cells visible from the {@link #getLocation()} + */ + List getVisible(); - @Override - public ITurtle getPainter() { - return painter; - } + /** + * @return Width of the map + */ + int getWidth(); - @Override - public Printer getPrinter() { - return printer; - } + /** + * Move the current actor in the given direction. + *

    + * The new location will be returned. + * + * @param dir + * @return A new location + * @throws IllegalMoveException if moving in that direction is illegal + */ + Location move(GridDirection dir); - @Override - public int[] getFreeTextAreaBounds() { - int[] area = new int[4]; - area[0] = getWidth() + 1; - area[1] = 1; - area[2] = printer.getLineWidth(); - area[3] = printer.getPageHeight() - 5; - return area; - } + /** + * Perform a ranged attack on the target. + *

    + * Rules for this are up to you! + * + * @param dir Direction + * @param target A target item, which should in some square in the given direction + * @return Your new location if the attack resulted in you moving (unlikely) + */ + Location rangedAttack(GridDirection dir, Item target, AttackType type); - @Override - public void clearFreeTextArea() { - printer.clearRegion(getWidth() + 1, 1, printer.getLineWidth() - getWidth(), printer.getPageHeight() - 5); - } + /** + * @return A random generator + */ + Random getRandom(); - @Override - public void clearFreeGraphicsArea() { - painter.as(GraphicsContext.class).clearRect(getWidth() * printer.getCharWidth(), 0, - painter.getWidth() - getWidth() * printer.getCharWidth(), - (printer.getPageHeight() - 5) * printer.getCharHeight()); - } - - @Override - public double[] getFreeGraphicsAreaBounds() { - double[] area = new double[4]; - area[0] = getWidth() * printer.getCharWidth(); - area[1] = 0; - area[2] = painter.getWidth(); - area[3] = getHeight() * printer.getCharHeight(); - return area; - } - - @Override - public IActor getActor() { - return currentActor; - } - - public ILocation setCurrent(IActor actor) { - currentLocation = map.getLocation(actor); - if (currentLocation != null) { - currentActor = actor; - movePoints = 1; - } - return currentLocation; - } - - public IActor setCurrent(ILocation loc) { - List list = map.getActors(loc); - if (!list.isEmpty()) { - currentActor = list.get(0); - currentLocation = loc; - movePoints = 1; - } - return currentActor; - } - - public IActor setCurrent(int x, int y) { - return setCurrent(map.getLocation(x, y)); - } - - @Override - public Random getRandom() { - return random; - } - - @Override - public List locationDirection(ILocation start, ILocation target) { - int targetX = target.getX(), targetY = target.getY(); - int startX = start.getX(), startY = start.getY(); - List dirs = new ArrayList<>(); - if (targetX > startX && targetY > startY) { - if (Math.abs(targetX - startX) < Math.abs(targetY - startY)) { - dirs.add(GridDirection.SOUTH); - dirs.add(GridDirection.EAST); - } else { - dirs.add(GridDirection.EAST); - dirs.add(GridDirection.SOUTH); - } - } else if (targetX > startX && targetY < startY) { - if (Math.abs(targetX - startX) < Math.abs(targetY - startY)) { - dirs.add(GridDirection.NORTH); - dirs.add(GridDirection.EAST); - } else { - dirs.add(GridDirection.EAST); - dirs.add(GridDirection.NORTH); - } - } else if (targetX < startX && targetY > startY) { - if (Math.abs(targetX - startX) < Math.abs(targetY - startY)) { - dirs.add(GridDirection.SOUTH); - dirs.add(GridDirection.WEST); - } else { - dirs.add(GridDirection.WEST); - dirs.add(GridDirection.SOUTH); - } - } else if (targetX < startX && targetY < startY) { - if (Math.abs(targetX - startX) < Math.abs(targetY - startY)) { - dirs.add(GridDirection.NORTH); - dirs.add(GridDirection.WEST); - } else { - dirs.add(GridDirection.WEST); - dirs.add(GridDirection.NORTH); - } - } else if (targetX > startX) { - dirs.add(GridDirection.EAST); - } else if (targetX < startX) { - dirs.add(GridDirection.WEST); - } else if (targetY > startY) { - dirs.add(GridDirection.SOUTH); - } else if (targetY < startY) { - dirs.add(GridDirection.NORTH); - } - return dirs; - } + /** + * Gets a list of the best directions to go from current to neighbour. + * If the target is positioned diagonally from the start, the direction requiring the most steps is chosen. + * + * @param current The location to go from + * @param neighbour The location to go to + * @return A direction + */ + List locationDirection(Location current, Location neighbour); } diff --git a/src/main/java/inf101/v18/rogue101/game/IGame.java b/src/main/java/inf101/v18/rogue101/game/IGame.java deleted file mode 100644 index 4d1cb2f..0000000 --- a/src/main/java/inf101/v18/rogue101/game/IGame.java +++ /dev/null @@ -1,340 +0,0 @@ -package inf101.v18.rogue101.game; - -import java.util.List; -import java.util.Random; - -import inf101.v18.gfx.gfxmode.ITurtle; -import inf101.v18.gfx.textmode.Printer; -import inf101.v18.grid.GridDirection; -import inf101.v18.grid.ILocation; -import inf101.v18.rogue101.map.IMapView; -import inf101.v18.rogue101.objects.IItem; -import inf101.v18.rogue101.objects.IActor; -import inf101.v18.rogue101.objects.INonPlayer; -import inf101.v18.rogue101.objects.IPlayer; -import inf101.v18.rogue101.states.Attack; - -/** - * Game interface - *

    - * The game has a map and a current {@link IActor} (the player or non-player - * whose turn it currently is). The game also knows the current location of the - * actor. Most methods that deal with the map will use this current location – - * they are meant to be used by the current actor for exploring or interacting - * with its surroundings. - *

    - * In other words, you should avoid calling most of these methods if you're not - * the current actor. You know you're the current actor when you're inside your - * {@link IPlayer#keyPressed()} or {@link INonPlayer#doTurn()} method. - * - * @author anya - * - */ -public interface IGame { - /** - * Add an item to the current location - *

    - * If the item is an actor, it won't move until the next turn. - * - * @param item - */ - void addItem(IItem item); - - /** - * Add a new item (identified by symbol) to the current location - *

    - * If the item is an actor, it won't move until the next turn. - * - * @param sym - */ - void addItem(String sym); - - /** - * Perform an attack on the target. - *

    - * Will use your {@link IActor#getAttack()} against the target's - * {@link IItem#getDefence()}, and then possiblyu find the damage you're dealing - * with {@link IActor#getDamage()} and inform the target using - * {@link IItem#handleDamage(IGame, IItem, int)} - * - * @param dir - * Direction - * @param target - * A target item, which should be in the neighbouring square in the - * given direction - * @return Your new location if the attack resulted in you moving - */ - ILocation attack(GridDirection dir, IItem target); - - /** - * @param dir - * @return True if it's possible to move in the given direction - */ - boolean canGo(GridDirection dir); - - /** - * Create a new item based on a text symbol - *

    - * The item won't be added to the map unless you call {@link #addItem(IItem)}. - * - * @param symbol - * @return The new item - */ - IItem createItem(String symbol); - - /** - * Displays a message in the debug area on the screen (bottom line) - * - * @param s - * A message - */ - void displayDebug(String s); - - /** - * Displays a message in the message area on the screen (below the map and the - * status line) - * - * @param s - * A message - */ - void displayMessage(String s); - - /** - * Displays a status message in the status area on the screen (right below the - * map) - * - * @param s - * A message - */ - void displayStatus(String s); - - /** - * Displays a message in the message area on the screen (below the map and the - * status line) - * - * @param s - * A message - * @see String#format(String, Object...) - */ - void formatDebug(String s, Object... args); - - /** - * Displays a formatted message in the message area on the screen (below the map - * and the status line) - * - * @param s - * A message - * @see String#format(String, Object...) - */ - void formatMessage(String s, Object... args); - - /** - * Displays a formatted status message in the status area on the screen (right - * below the map) - * - * @param s - * A message - * @see String#format(String, Object...) - */ - void formatStatus(String s, Object... args); - - /** - * Pick up an item - *

    - * This should be used by IActors who want to pick up an item and carry it. The - * item will be returned if picking it succeeded (the actor might also - * make a mistake and pick up the wrong item!). - * - * @param item - * An item, should be in the current map location - * @return The item that was picked up (normally item), or - * null if it failed - */ - IItem pickUp(IItem item); - - /** - * Drop an item - *

    - * This should be used by IActors who are carrying an item and want to put it on - * the ground. Check the return value to see if it succeeded. - * - * @param item - * An item, should be carried by the current actor and not already be - * on the map - * @return True if the item was placed on the map, false means you're still - * holding it - */ - boolean drop(IItem item); - - /** - * Does the same as drop, but for a specified location. - * - * @param loc The location to drop the location - * @param item The item to drop - * @return True if the item was dropped. False otherwise - */ - boolean dropAt(ILocation loc, IItem item); - - /** - * Clear the unused graphics area (you can fill it with whatever you want!) - */ - void clearFreeGraphicsArea(); - - /** - * Clear the unused text area (you can fill it with whatever you want!) - */ - void clearFreeTextArea(); - - /** - * Get the bounds of the free graphics area. - *

    - * You can fill this with whatever you want, using {@link #getPainter()} and - * {@link #clearFreeGraphicsArea()}. - * - * @return Array of coordinates; ([0],[1]) are the top-left corner, and - * ([2],[3]) are the bottom-right corner (exclusive). - */ - double[] getFreeGraphicsAreaBounds(); - - /** - * Get the bounds of the free text area. - *

    - * You can fill this with whatever you want, using {@link #getPrinter()} and - * {@link #clearFreeTextArea()}. - *

    - * You'll probably want to use something like: - * - *

    -	 * int[] bounds = getFreeTextArea();
    -	 * int x = bounds[0];
    -	 * int y = bounds[1];
    -	 * game.getPrinter().printAt(x, y++, "Hello");
    -	 * game.getPrinter().printAt(x, y++, "Do you have any carrot cake?", Color.ORANGE);
    -	 * 
    - * - * @return Array of column/line numbers; ([0],[1]) is the top-left corner, and - * ([2],[3]) is the bottom-right corner (inclusive). - */ - int[] getFreeTextAreaBounds(); - - /** - * See {@link #getFreeGraphicsAreaBounds()}, {@link #clearFreeGraphicsArea()}. - * - * @return A Turtle, for painting graphics - */ - ITurtle getPainter(); - - /** - * See {@link #getFreeTextAreaBounds()}, {@link #clearFreeTextArea()}. - * - * @return A printer, for printing text - */ - Printer getPrinter(); - - /** - * @return The height of the map - */ - int getHeight(); - - /** - * @return A list of the non-actor items at the current map location - */ - List getLocalItems(); - - /** - * Get the current actor's location. - *

    - * You should only call this from an IActor that is currently doing its move. - * - * @return Location of the current actor - */ - ILocation getLocation(); - - /** - * Get the current actor - *

    - * You can check if it's your move by doing game.getActor()==this. - * - * @return The current actor (i.e., the (IPlayer/INonPlayer) player who's turn it currently is) - */ - IActor getActor(); - - /** - * Get the neighbouring map location in direction dir - *

    - * Same as getLocation().go(dir) - * - * @param dir - * A direction - * @return A location, or null if the location would be outside the - * map - */ - ILocation getLocation(GridDirection dir); - - /** - * @return The map - */ - IMapView getMap(); - - /** - * @return A list of directions we can move in, for use with - * {@link #move(GridDirection)} - */ - List getPossibleMoves(); - - /** - * Get a list of all locations that are visible from the current location. - *

    - * The location list is sorted so that nearby locations come earlier in the - * list. E.g., if l = getVisible() and i < jthen - * getLocation().gridDistanceTo(l.get(i)) < getLocation().gridDistanceTo(l.get(j)) - * - * @return A list of grid cells visible from the {@link #getLocation()} - */ - List getVisible(); - - /** - * @return Width of the map - */ - int getWidth(); - - /** - * Move the current actor in the given direction. - *

    - * The new location will be returned. - * - * @param dir - * @return A new location - * @throws IllegalMoveException - * if moving in that direction is illegal - */ - ILocation move(GridDirection dir); - - /** - * Perform a ranged attack on the target. - *

    - * Rules for this are up to you! - * - * @param dir - * Direction - * @param target - * A target item, which should in some square in the given direction - * @return Your new location if the attack resulted in you moving (unlikely) - */ - ILocation rangedAttack(GridDirection dir, IItem target, Attack type); - - /** - * @return A random generator - */ - Random getRandom(); - - /** - * Gets a list of the best directions to go from current to neighbour. - * If the target is positioned diagonally from the start, the direction requiring the most steps is chosen. - * - * @param current The location to go from - * @param neighbour The location to go to - * @return A direction - */ - List locationDirection(ILocation current, ILocation neighbour); -} diff --git a/src/main/java/inf101/v18/rogue101/game/IllegalMoveException.java b/src/main/java/inf101/v18/rogue101/game/IllegalMoveException.java index 6bf1862..98d9509 100644 --- a/src/main/java/inf101/v18/rogue101/game/IllegalMoveException.java +++ b/src/main/java/inf101/v18/rogue101/game/IllegalMoveException.java @@ -3,18 +3,17 @@ package inf101.v18.rogue101.game; /** * An exception to be thrown when an moving object tries to do an illegal move, * for example to a position outside the map. - * - * @author larsjaffke * + * @author larsjaffke */ public class IllegalMoveException extends RuntimeException { - /** - * - */ - private static final long serialVersionUID = 7641529271996915740L; + /** + * + */ + private static final long serialVersionUID = 7641529271996915740L; - public IllegalMoveException(String message) { - super(message); - } + public IllegalMoveException(String message) { + super(message); + } } diff --git a/src/main/java/inf101/v18/rogue101/game/RogueGame.java b/src/main/java/inf101/v18/rogue101/game/RogueGame.java new file mode 100644 index 0000000..fbbe638 --- /dev/null +++ b/src/main/java/inf101/v18/rogue101/game/RogueGame.java @@ -0,0 +1,852 @@ +package inf101.v18.rogue101.game; + +import inf101.v18.gfx.Screen; +import inf101.v18.gfx.gfxmode.ITurtle; +import inf101.v18.gfx.gfxmode.TurtlePainter; +import inf101.v18.gfx.textmode.Printer; +import inf101.v18.grid.GridDirection; +import inf101.v18.grid.IGrid; +import inf101.v18.grid.Location; +import inf101.v18.rogue101.Main; +import inf101.v18.rogue101.enemies.Boss; +import inf101.v18.rogue101.enemies.Girl; +import inf101.v18.rogue101.examples.Carrot; +import inf101.v18.rogue101.examples.Rabbit; +import inf101.v18.rogue101.items.buff.Binoculars; +import inf101.v18.rogue101.items.buff.BuffItem; +import inf101.v18.rogue101.items.buff.Shield; +import inf101.v18.rogue101.items.consumable.HealthPotion; +import inf101.v18.rogue101.items.consumable.Manga; +import inf101.v18.rogue101.items.container.Chest; +import inf101.v18.rogue101.items.container.Static; +import inf101.v18.rogue101.items.weapon.BasicBow; +import inf101.v18.rogue101.items.weapon.BasicSword; +import inf101.v18.rogue101.items.weapon.FireStaff; +import inf101.v18.rogue101.items.weapon.MagicWeapon; +import inf101.v18.rogue101.items.weapon.MeleeWeapon; +import inf101.v18.rogue101.items.weapon.RangedWeapon; +import inf101.v18.rogue101.items.weapon.Weapon; +import inf101.v18.rogue101.map.AGameMap; +import inf101.v18.rogue101.map.GameMap; +import inf101.v18.rogue101.map.MapReader; +import inf101.v18.rogue101.map.MapView; +import inf101.v18.rogue101.object.Actor; +import inf101.v18.rogue101.object.Dust; +import inf101.v18.rogue101.object.FakeWall; +import inf101.v18.rogue101.object.Item; +import inf101.v18.rogue101.object.NonPlayerCharacter; +import inf101.v18.rogue101.object.Player; +import inf101.v18.rogue101.object.PlayerCharacter; +import inf101.v18.rogue101.object.StairsDown; +import inf101.v18.rogue101.object.StairsUp; +import inf101.v18.rogue101.object.Wall; +import inf101.v18.rogue101.state.AttackType; +import inf101.v18.rogue101.state.Sound; +import inf101.v18.util.NPCHelper; +import javafx.scene.canvas.GraphicsContext; +import javafx.scene.input.KeyCode; +import javafx.scene.paint.Color; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashMap; +import java.util.Iterator; +import java.util.List; +import java.util.Map; +import java.util.Random; +import java.util.function.Supplier; + +public class RogueGame implements Game { + /** + * All the IActors that have things left to do this turn + */ + private List actors = Collections.synchronizedList(new ArrayList<>()); + /** + * For fancy solution to factory problem + */ + private final Map> itemFactories = new HashMap<>(); + /** + * Useful random generator + */ + private final Random random = new Random(); + + /** + * Saves the last three messages + */ + private final List lastMessages = new ArrayList<>(); + + /** + * The game map. {@link GameMap} gives us a few more details than + * {@link MapView} (write access to item lists); the game needs this but + * individual items don't. + */ + private GameMap map; + private final List maps = new ArrayList<>(); + private int currentLVL = 0; + private Actor currentActor; + private Location currentLocation; + private int movePoints = 0; + private final ITurtle painter; + private final Printer printer; + private int numberOfPlayers = 0; + private boolean won = false; + private List visible = null; + + public RogueGame(Screen screen, ITurtle painter, Printer printer) { + this.painter = painter; + this.printer = printer; + + addFactory(); + + // NOTE: in a more realistic situation, we will have multiple levels (one map + // per level), and (at least for a Roguelike game) the levels should be + // generated + // + // inputGrid will be filled with single-character strings indicating what (if + // anything) + // should be placed at that map square + loadMap("level1.txt", 1); + loadMap("level2.txt", 2); + loadMap("level3.txt", 3); + loadMap("level4.txt", 4); + loadMap("level5.txt", 5); + map = maps.get(0); + map.add(map.getLocation(3, 3), new Player()); //We must be sure there is never more than one player + + // Prints some helpful information. + String[] info = {"Controls:", "WASD or arrow keys for movement", "E to pick up an item", "Q to drop an item", "1-0 to choose an item (10=0)", "N to change name", "ENTER to confirm", "R to use a ranged attack", "F to use a magic attack", "C to use a consumable"}; + for (int i = 0; i < info.length; i++) { + this.printer.printAt(Main.COLUMN_RIGHT_SIDE_START, 1 + i, info[i]); + } + } + + public RogueGame(String mapString) { + printer = new Printer(1280, 720); + painter = new TurtlePainter(1280, 720); + + addFactory(); + + IGrid inputGrid = MapReader.readString(mapString); + this.map = new AGameMap(inputGrid.getArea()); + for (Location loc : inputGrid.locations()) { + Item item = createItem(inputGrid.get(loc)); + if (item != null) { + map.add(loc, item); + } + } + } + + @Override + public void addItem(Item item) { + map.add(currentLocation, item); + } + + @Override + public void addItem(String sym) { + Item item = createItem(sym); + if (item != null) { + map.add(currentLocation, item); + } + } + + /** + * Goes up one floor if possible. + */ + public void goUp() { + if (currentLVL + 1 < maps.size()) { + map = maps.get(++currentLVL); + if (!map.has(currentLocation, currentActor)) { + map.add(currentLocation, currentActor); + } + actors = new ArrayList<>(); + displayMessage("You went up the stairs"); + } + } + + /** + * Goes down one floor if possible. + */ + public void goDown() { + if (currentLVL > 0) { + map = maps.get(--currentLVL); + if (!map.has(currentLocation, currentActor)) { + map.add(currentLocation, currentActor); + } + actors = new ArrayList<>(); + displayMessage("You went down the stairs"); + } + } + + /** + * Calculates the attack of an IActor based on equipped items. + * + * @return The attack + */ + private int getAttack(AttackType type) { + int attack = currentActor.getAttack() + random.nextInt(20) + 1; + Weapon weapon = NPCHelper.getWeapon(type, currentActor); + if (weapon != null) { + attack += weapon.getWeaponAttack(); + } + return attack; + } + + /** + * Gets the defence of the current target. + * + * @param target The target to evaluate + * @return The defence of the target + */ + private int getDefence(Item target) { + int defence = target.getDefense() + 10; + Actor actor = (Actor) target; + BuffItem item = (BuffItem) actor.getItem(BuffItem.class); + if (item != null) { + defence += item.getBuffDefene(); + } + return defence; + } + + /** + * Gets the damage done against the current target. + * + * @param target The target to evaluate. + * @return The damage done to the target. + */ + private int getDamage(Item target, AttackType type) { + int damage = currentActor.getDamage(); + Weapon weapon = NPCHelper.getWeapon(type, currentActor); + if (weapon != null) { + damage += weapon.getWeaponDamage(); + } + BuffItem buff = (BuffItem) currentActor.getItem(BuffItem.class); + if (buff != null) { + damage += buff.getBuffDamage(); + } + BuffItem item = (BuffItem) ((Actor) target).getItem(BuffItem.class); + if (item != null) { + damage -= item.getBuffDamageReduction(); + } + return damage; + } + + @Override + public Location attack(GridDirection dir, Item target) { + Location loc = currentLocation.go(dir); + if (!map.has(loc, target)) { + throw new IllegalMoveException("Target isn't there!"); + } + Weapon weapon = (Weapon) currentActor.getItem(MeleeWeapon.class); + if (weapon != null) { + weapon.getSound().play(); + } else { + Sound.MELEE_NO_WEAPON.play(); + } + if (getAttack(AttackType.MELEE) >= getDefence(target)) { + int actualDamage = target.handleDamage(this, target, getDamage(target, AttackType.MELEE)); + if (currentActor != null) { + formatMessage("%s hits %s for %d damage", currentActor.getName(), target.getName(), actualDamage); + } + } else { + formatMessage("%s tried to hit %s, but missed", currentActor.getName(), target.getName()); + } + + map.clean(loc); + + if (target.isDestroyed() && currentLocation != null) { + return move(dir); + } else { + movePoints--; + return currentLocation; + } + } + + @Override + public Location rangedAttack(GridDirection dir, Item target, AttackType type) { + Location loc = currentLocation; + Weapon weapon = null; + switch (type) { + case MAGIC: + weapon = (Weapon) currentActor.getItem(MagicWeapon.class); + break; + case RANGED: + weapon = (Weapon) currentActor.getItem(RangedWeapon.class); + } + if (weapon != null) { + weapon.getSound().play(); + } else { + Sound.RANGED_NO_WEAPON.play(); + } + if (getAttack(type) >= getDefence(target)) { + int damage = getDamage(target, type) / loc.gridDistanceTo(map.getLocation(target)); + int actualDamage = target.handleDamage(this, target, damage); + formatMessage("%s hits %s for %d damage", currentActor.getName(), target.getName(), actualDamage); + } else { + formatMessage("%s tried to hit %s, but missed", currentActor.getName(), target.getName()); + } + map.clean(map.getLocation(target)); + if (target.isDestroyed() && map.has(currentLocation.go(dir), target)) { + return move(dir); + } else { + return currentLocation; + } + } + + /** + * Begin a new game turn, or continue to the previous turn + * + * @return True if the game should wait for more user input + */ + public boolean doTurn() { + do { + if (actors.isEmpty()) { + // System.err.println("new turn!"); + + // no one in the queue, we're starting a new turn! + // first collect all the actors: + beginTurn(); + } + + /*if (random.nextInt(100) < 20) { + ILocation loc = map.getLocation(random.nextInt(map.getWidth()), random.nextInt(map.getHeight())); + if (!map.hasActors(loc) && !map.hasItems(loc) && !map.hasWall(loc)) { + map.add(loc, new Carrot()); + } + }*/ //We don't want this in the actual game. + + // process actors one by one; for the IPlayer, we return and wait for keypresses + // Possible for INonPlayer, we could also return early (returning + // *false*), and then insert a little timer delay between each non-player move + // (the timer + // is already set up in Main) + if (numberOfPlayers == 0 && !won) { + kill(); + } + while (!actors.isEmpty()) { + // get the next player or non-player in the queue + currentActor = actors.remove(0); + if (currentActor == null) { + return false; //TODO: Find out why a girl suddenly becomes null (only after map change, not caught in beginTurn, happens randomly) + } + // skip if it's dead + if (currentActor.isDestroyed()) { + continue; + } + currentLocation = map.getLocation(currentActor); + if (currentLocation == null) { + displayDebug("doTurn(): Whoops! Actor has disappeared from the map: " + currentActor); + } + movePoints = 1; // everyone gets to do one thing + visible = null; + + if (currentActor instanceof NonPlayerCharacter) { + // computer-controlled players do their stuff right away + ((NonPlayerCharacter) currentActor).doTurn(this); + // remove any dead items from current location + map.clean(currentLocation); + return false; + } else if (currentActor instanceof PlayerCharacter) { + if (!currentActor.isDestroyed()) { + // For the human player, we need to wait for input, so we just return. + // Further keypresses will cause keyPressed() to be called, and once the human + // makes a move, it'll lose its movement point and doTurn() will be called again + // + // NOTE: currentActor and currentLocation are set to the IPlayer (above), + // so the game remembers who the player is whenever new keypresses occur. This + // is also how e.g., getLocalItems() work – the game always keeps track of + // whose turn it is. + return true; + } + } else { + displayDebug("doTurn(): Hmm, this is a very strange actor: " + currentActor); + } + } + } while (numberOfPlayers > 0); // we can safely repeat if we have players, since we'll return (and break out of + // the loop) once we hit the player + return true; + } + + /** + * Player is dead. It needs to be properly killed off. + */ + private void kill() { + map.remove(currentLocation, currentActor); + currentActor = null; + currentLocation = null; + actors = new ArrayList<>(); + loadMap("gameover.txt"); + Sound.GAME_OVER.play(); + } + + public void win() { //Trigger when the boss dies + map.remove(currentLocation, currentActor); + currentActor = null; + currentLocation = null; + actors = new ArrayList<>(); + loadMap("victory.txt"); + map.draw(painter, printer); + Sound.WIN.play(); + won = true; + } + + /** + * Loads a map with the desired name + * + * @param mapName Name of map, including extension. + */ + private void loadMap(String mapName) { + IGrid inputGrid = MapReader.readFile("maps/" + mapName); + if (inputGrid == null) { + System.err.println("Map not found – falling back to builtin map"); + inputGrid = MapReader.readString(Main.BUILTIN_MAP); + } + GameMap map = new AGameMap(inputGrid.getArea()); + for (Location loc : inputGrid.locations()) { + Item item = createItem(inputGrid.get(loc)); + if (item != null) { + map.add(loc, item); + } + } + this.map = map; + } + + private void loadMap(String mapName, int lvl) { + IGrid inputGrid = MapReader.readFile("maps/" + mapName); + if (inputGrid == null) { + System.err.println("Map not found – falling back to builtin map"); + inputGrid = MapReader.readString(Main.BUILTIN_MAP); + } + GameMap map = new AGameMap(inputGrid.getArea()); + for (Location loc : inputGrid.locations()) { + Item item = createItem(inputGrid.get(loc)); + if (item instanceof Chest) { + ((Chest) item).fill(lvl); + } else if (item instanceof Girl) { + ((Girl) item).giveWeapon(lvl); + } + if (item != null) { + map.add(loc, item); + } + } + maps.add(map); + } + + /** + * Go through the map and collect all the actors. + */ + private void beginTurn() { + numberOfPlayers = 0; + // this extra fancy iteration over each map location runs *in parallel* on + // multicore systems! + // that makes some things more tricky, hence the "synchronized" block and + // "Collections.synchronizedList()" in the initialization of "actors". + // NOTE: If you want to modify this yourself, it might be a good idea to replace + // "parallelStream()" by "stream()", because weird things can happen when many + // things happen + // at the same time! (or do INF214 or DAT103 to learn about locks and threading) + map.getArea().parallelStream().forEach((loc) -> { // will do this for each location in map + List list = map.getAllModifiable(loc); // all items at loc + Iterator li = list.iterator(); // manual iterator lets us remove() items + while (li.hasNext()) { // this is what "for(IItem item : list)" looks like on the inside + Item item = li.next(); + if (item.getCurrentHealth() < 0) { + // normally, we expect these things to be removed when they are destroyed, so + // this shouldn't happen + synchronized (this) { + formatDebug("beginTurn(): found and removed leftover destroyed item %s '%s' at %s%n", + item.getName(), item.getSymbol(), loc); + } + li.remove(); + map.remove(loc, item); // need to do this too, to update item map + } else if (item instanceof PlayerCharacter) { + actors.add(0, (Actor) item); // we let the human player go first + synchronized (this) { + numberOfPlayers++; + } + } else if (item instanceof Actor) { + actors.add((Actor) item); // add other actors to the end of the list + } else if (item instanceof Carrot) { + ((Carrot) item).doTurn(); + } + } + }); + } + + @Override + public boolean canGo(GridDirection dir) { + return map.canGo(currentLocation, dir); + } + + private void addFactory() { + itemFactories.put("#", Wall::new); + itemFactories.put("@", Player::new); + itemFactories.put("C", Carrot::new); + itemFactories.put("R", Rabbit::new); + itemFactories.put("M", Manga::new); + itemFactories.put("G", Girl::new); + itemFactories.put(".", Dust::new); + itemFactories.put("S", BasicSword::new); + itemFactories.put("c", Chest::new); + itemFactories.put("B", Boss::new); + itemFactories.put("s", FireStaff::new); + itemFactories.put("b", BasicBow::new); + itemFactories.put("H", HealthPotion::new); + itemFactories.put("=", FakeWall::new); + itemFactories.put("<", StairsUp::new); + itemFactories.put(">", StairsDown::new); + itemFactories.put("m", Binoculars::new); + itemFactories.put("f", Shield::new); + } + + @Override + public Item createItem(String symbol) { + if (" ".equals(symbol)) { + return null; + }// alternative/advanced method + Supplier factory = itemFactories.get(symbol); + if (factory != null) { + return factory.get(); + } else { + System.err.println("createItem: Don't know how to create a '" + symbol + "'"); + return null; + } + } + + @Override + public void displayDebug(String s) { + printer.clearLine(Main.LINE_DEBUG); + printer.printAt(1, Main.LINE_DEBUG, s, Color.DARKRED); + System.err.println(s); + } + + @Override + public void displayMessage(String s) { + if (lastMessages.size() >= 3) { + lastMessages.remove(2); + } + lastMessages.add(0, s); + printer.clearLine(Main.LINE_MSG1); + printer.clearLine(Main.LINE_MSG2); + printer.clearLine(Main.LINE_MSG3); + + int maxLen = 80; //The maximum length of a message to not overflow. + boolean secondLineWritten = false; + boolean thirdLineWritten = false; + + //Makes long messages overflow to the next line. + if (lastMessages.size() > 0) { + String message = lastMessages.get(0); + if (message.length() > 2 * maxLen) { + printer.printAt(1, Main.LINE_MSG1, message.substring(0, maxLen)); + printer.printAt(1, Main.LINE_MSG2, message.substring(maxLen, 2 * maxLen)); + printer.printAt(1, Main.LINE_MSG3, message.substring(2 * maxLen)); + secondLineWritten = thirdLineWritten = true; + } else if (message.length() > maxLen) { + printer.printAt(1, Main.LINE_MSG1, message.substring(0, maxLen)); + printer.printAt(1, Main.LINE_MSG2, message.substring(maxLen)); + secondLineWritten = true; + } else { + printer.printAt(1, Main.LINE_MSG1, message); + } + } + if (lastMessages.size() > 1 && !secondLineWritten) { + String message = lastMessages.get(1); + if (message.length() > maxLen) { + printer.printAt(1, Main.LINE_MSG2, message.substring(0, maxLen)); + printer.printAt(1, Main.LINE_MSG3, message.substring(maxLen)); + thirdLineWritten = true; + } else { + printer.printAt(1, Main.LINE_MSG2, message); + } + } + if (lastMessages.size() > 2 && !thirdLineWritten) { + printer.printAt(1, Main.LINE_MSG3, lastMessages.get(2)); + } + //System.out.println("Message: «" + s + "»"); + } + + @Override + public void displayStatus(String s) { + printer.clearLine(Main.LINE_STATUS); + printer.printAt(1, Main.LINE_STATUS, s); + //System.out.println("Status: «" + s + "»"); + } + + public void draw() { + if (numberOfPlayers == 0) { + map.draw(painter, printer); + } else { + ((AGameMap) map).drawVisible(painter, printer); + } + } + + @Override + public boolean drop(Item item) { + if (item != null) { + map.add(currentLocation, item); + return true; + } else { + return false; + } + } + + @Override + public boolean dropAt(Location loc, Item item) { + if (item != null) { + map.add(loc, item); + return true; + } else { + return false; + } + } + + @Override + public void formatDebug(String s, Object... args) { + displayDebug(String.format(s, args)); + } + + @Override + public void formatMessage(String s, Object... args) { + displayMessage(String.format(s, args)); + } + + @Override + public void formatStatus(String s, Object... args) { + displayStatus(String.format(s, args)); + } + + @Override + public int getHeight() { + return map.getHeight(); + } + + @Override + public List getLocalItems() { + return map.getItems(currentLocation); + } + + @Override + public Location getLocation() { + return currentLocation; + } + + @Override + public Location getLocation(GridDirection dir) { + if (currentLocation.canGo(dir)) { + return currentLocation.go(dir); + } else { + return null; + } + } + + /** + * Return the game map. {@link GameMap} gives us a few more details than + * {@link MapView} (write access to item lists); the game needs this but + * individual items don't. + */ + @Override + public MapView getMap() { + return map; + } + + @Override + public List getPossibleMoves() { + List moves = new ArrayList<>(); + for (GridDirection dir : GridDirection.FOUR_DIRECTIONS) { + if (canGo(dir)) { + moves.add(dir); + } + } + return moves; + } + + @Override + public List getVisible() { + if (this.visible != null) { + return this.visible; + } + List neighbours = this.map.getNeighbourhood(this.currentLocation, this.currentActor.getVision()); + List invalid = new ArrayList<>(); + for (Location neighbour : neighbours) { + for (Location tile : this.currentLocation.gridLineTo(neighbour)) { + if (this.map.hasWall(tile)) { + invalid.add(neighbour); + break; + } + } + } + neighbours.removeAll(invalid); + this.visible = neighbours; + return neighbours; + } + + @Override + public int getWidth() { + return this.map.getWidth(); + } + + public boolean keyPressed(KeyCode code) { + // only an IPlayer/human can handle keypresses, and only if it's the human's + // turn + return !(currentActor instanceof PlayerCharacter) || !((PlayerCharacter) currentActor).keyPressed(this, code); + } + + @Override + public Location move(GridDirection dir) { + if (movePoints < 1) { + throw new IllegalMoveException("You're out of moves!"); + } + Location newLoc = map.go(currentLocation, dir); + map.remove(currentLocation, currentActor); + map.add(newLoc, currentActor); + currentLocation = newLoc; + movePoints--; + return currentLocation; + } + + @Override + public Item pickUp(Item item) { + if (item != null && map.has(currentLocation, item) && !(item instanceof Static)) { + if (item instanceof Actor) { + if (item.getCurrentHealth() / item.getMaxHealth() < 3) { + map.remove(currentLocation, item); + return item; + } else { + return null; + } + } else if (currentActor.getAttack() > item.getDefense()) { + map.remove(currentLocation, item); + return item; + } else { + return null; + } + } else { + return null; + } + } + + @Override + public ITurtle getPainter() { + return painter; + } + + @Override + public Printer getPrinter() { + return printer; + } + + @Override + public int[] getFreeTextAreaBounds() { + int[] area = new int[4]; + area[0] = getWidth() + 1; + area[1] = 1; + area[2] = printer.getLineWidth(); + area[3] = printer.getPageHeight() - 5; + return area; + } + + @Override + public void clearFreeTextArea() { + printer.clearRegion(getWidth() + 1, 1, printer.getLineWidth() - getWidth(), printer.getPageHeight() - 5); + } + + @Override + public void clearFreeGraphicsArea() { + painter.as(GraphicsContext.class).clearRect(getWidth() * printer.getCharWidth(), 0, + painter.getWidth() - getWidth() * printer.getCharWidth(), + (printer.getPageHeight() - 5) * printer.getCharHeight()); + } + + @Override + public double[] getFreeGraphicsAreaBounds() { + double[] area = new double[4]; + area[0] = getWidth() * printer.getCharWidth(); + area[1] = 0; + area[2] = painter.getWidth(); + area[3] = getHeight() * printer.getCharHeight(); + return area; + } + + @Override + public Actor getActor() { + return currentActor; + } + + public Location setCurrent(Actor actor) { + currentLocation = map.getLocation(actor); + if (currentLocation != null) { + currentActor = actor; + movePoints = 1; + } + return currentLocation; + } + + public Actor setCurrent(Location loc) { + List list = map.getActors(loc); + if (!list.isEmpty()) { + currentActor = list.get(0); + currentLocation = loc; + movePoints = 1; + } + return currentActor; + } + + public Actor setCurrent(int x, int y) { + return setCurrent(map.getLocation(x, y)); + } + + @Override + public Random getRandom() { + return random; + } + + @Override + public List locationDirection(Location start, Location target) { + int targetX = target.getX(), targetY = target.getY(); + int startX = start.getX(), startY = start.getY(); + List dirs = new ArrayList<>(); + boolean yDifferenceIsLarger = Math.abs(targetX - startX) < Math.abs(targetY - startY); + if (targetX > startX && targetY > startY) { + if (yDifferenceIsLarger) { + dirs.add(GridDirection.SOUTH); + dirs.add(GridDirection.EAST); + } else { + dirs.add(GridDirection.EAST); + dirs.add(GridDirection.SOUTH); + } + } else if (targetX > startX && targetY < startY) { + if (yDifferenceIsLarger) { + dirs.add(GridDirection.NORTH); + dirs.add(GridDirection.EAST); + } else { + dirs.add(GridDirection.EAST); + dirs.add(GridDirection.NORTH); + } + } else if (targetX < startX && targetY > startY) { + if (yDifferenceIsLarger) { + dirs.add(GridDirection.SOUTH); + dirs.add(GridDirection.WEST); + } else { + dirs.add(GridDirection.WEST); + dirs.add(GridDirection.SOUTH); + } + } else if (targetX < startX && targetY < startY) { + if (yDifferenceIsLarger) { + dirs.add(GridDirection.NORTH); + dirs.add(GridDirection.WEST); + } else { + dirs.add(GridDirection.WEST); + dirs.add(GridDirection.NORTH); + } + } else if (targetX > startX) { + dirs.add(GridDirection.EAST); + } else if (targetX < startX) { + dirs.add(GridDirection.WEST); + } else if (targetY > startY) { + dirs.add(GridDirection.SOUTH); + } else if (targetY < startY) { + dirs.add(GridDirection.NORTH); + } + return dirs; + } +} diff --git a/src/main/java/inf101/v18/rogue101/items/IConsumable.java b/src/main/java/inf101/v18/rogue101/items/IConsumable.java deleted file mode 100644 index 7266e8d..0000000 --- a/src/main/java/inf101/v18/rogue101/items/IConsumable.java +++ /dev/null @@ -1,9 +0,0 @@ -package inf101.v18.rogue101.items; - -import inf101.v18.rogue101.objects.IItem; - -public interface IConsumable extends IItem { - int hpIncrease(); - int attackIncrease(); - int defenceIncrease(); -} diff --git a/src/main/java/inf101/v18/rogue101/items/IMagicWeapon.java b/src/main/java/inf101/v18/rogue101/items/IMagicWeapon.java deleted file mode 100644 index d5f98bb..0000000 --- a/src/main/java/inf101/v18/rogue101/items/IMagicWeapon.java +++ /dev/null @@ -1,12 +0,0 @@ -package inf101.v18.rogue101.items; - -/** - * An interface to extinguish different weapons for specific NPC. - */ -public interface IMagicWeapon extends IWeapon { - - @Override - default String getSound() { - return "audio/Large Fireball-SoundBible.com-301502490.wav"; - } -} diff --git a/src/main/java/inf101/v18/rogue101/items/IMeleeWeapon.java b/src/main/java/inf101/v18/rogue101/items/IMeleeWeapon.java deleted file mode 100644 index ccdf971..0000000 --- a/src/main/java/inf101/v18/rogue101/items/IMeleeWeapon.java +++ /dev/null @@ -1,12 +0,0 @@ -package inf101.v18.rogue101.items; - -/** - * An interface to extinguish different weapons for specific NPC. - */ -public interface IMeleeWeapon extends IWeapon { - - @Override - default String getSound() { - return "audio/Sword Swing-SoundBible.com-639083727.wav"; - } -} diff --git a/src/main/java/inf101/v18/rogue101/items/IRangedWeapon.java b/src/main/java/inf101/v18/rogue101/items/IRangedWeapon.java deleted file mode 100644 index 416d579..0000000 --- a/src/main/java/inf101/v18/rogue101/items/IRangedWeapon.java +++ /dev/null @@ -1,12 +0,0 @@ -package inf101.v18.rogue101.items; - -/** - * An interface to extinguish different weapons for specific NPC. - */ -public interface IRangedWeapon extends IWeapon { - - @Override - default String getSound() { - return "audio/Bow_Fire_Arrow-Stephan_Schutze-2133929391.wav"; - } -} diff --git a/src/main/java/inf101/v18/rogue101/items/IStatic.java b/src/main/java/inf101/v18/rogue101/items/IStatic.java deleted file mode 100644 index ea26dfa..0000000 --- a/src/main/java/inf101/v18/rogue101/items/IStatic.java +++ /dev/null @@ -1,15 +0,0 @@ -package inf101.v18.rogue101.items; - -import inf101.v18.rogue101.objects.IItem; - -public interface IStatic extends IItem { - @Override - default int getDefence() { - return 0; - } - - @Override - default int getSize() { - return 10000; - } -} diff --git a/src/main/java/inf101/v18/rogue101/items/IWeapon.java b/src/main/java/inf101/v18/rogue101/items/IWeapon.java deleted file mode 100644 index 7f8af20..0000000 --- a/src/main/java/inf101/v18/rogue101/items/IWeapon.java +++ /dev/null @@ -1,26 +0,0 @@ -package inf101.v18.rogue101.items; - -import inf101.v18.rogue101.objects.IItem; - -public interface IWeapon extends IItem { - /** - * Retrieves the damage points of a weapon. - * - * @return An int - */ - int getWeaponDamage(); - - /** - * Returns the attack ponts of a weapon. - * - * @return an int - */ - int getWeaponAttack(); - - /** - * Retrieves the string path of the sound to play upon an attack. - * - * @return A string path - */ - String getSound(); -} diff --git a/src/main/java/inf101/v18/rogue101/items/Binoculars.java b/src/main/java/inf101/v18/rogue101/items/buff/Binoculars.java similarity index 70% rename from src/main/java/inf101/v18/rogue101/items/Binoculars.java rename to src/main/java/inf101/v18/rogue101/items/buff/Binoculars.java index 6b32899..7ec41a8 100644 --- a/src/main/java/inf101/v18/rogue101/items/Binoculars.java +++ b/src/main/java/inf101/v18/rogue101/items/buff/Binoculars.java @@ -1,9 +1,9 @@ -package inf101.v18.rogue101.items; +package inf101.v18.rogue101.items.buff; -import inf101.v18.rogue101.game.IGame; -import inf101.v18.rogue101.objects.IItem; +import inf101.v18.rogue101.game.Game; +import inf101.v18.rogue101.object.Item; -public class Binoculars implements IBuffItem { +public class Binoculars implements BuffItem { @Override public int getBuffVisibility() { @@ -16,7 +16,7 @@ public class Binoculars implements IBuffItem { } @Override - public int getDefence() { + public int getDefense() { return 0; } @@ -46,7 +46,7 @@ public class Binoculars implements IBuffItem { } @Override - public int handleDamage(IGame game, IItem source, int amount) { + public int handleDamage(Game game, Item source, int amount) { return 0; } } diff --git a/src/main/java/inf101/v18/rogue101/items/IBuffItem.java b/src/main/java/inf101/v18/rogue101/items/buff/BuffItem.java similarity index 60% rename from src/main/java/inf101/v18/rogue101/items/IBuffItem.java rename to src/main/java/inf101/v18/rogue101/items/buff/BuffItem.java index 6d112f2..bf8ac98 100644 --- a/src/main/java/inf101/v18/rogue101/items/IBuffItem.java +++ b/src/main/java/inf101/v18/rogue101/items/buff/BuffItem.java @@ -1,12 +1,16 @@ -package inf101.v18.rogue101.items; +package inf101.v18.rogue101.items.buff; -import inf101.v18.rogue101.objects.IItem; +import inf101.v18.rogue101.object.Item; + +/** + * An item that increases one or more of a player's stats + */ +public interface BuffItem extends Item { -public interface IBuffItem extends IItem { /** * Retrieve damage increase done by the buff item. * - * @return An int, May be 0 + * @return An int, May be 0 */ default int getBuffDamage() { return 0; @@ -15,16 +19,16 @@ public interface IBuffItem extends IItem { /** * Retrieve defence increase done by the buff item. * - * @return An int, May be 0 + * @return An int, May be 0 */ - default int getBuffDefence() { + default int getBuffDefene() { return 0; } /** * Retrieve defence increase done by the buff item. * - * @return An int, May be 0 + * @return An int, May be 0 */ default int getBuffDamageReduction() { return 0; @@ -33,9 +37,10 @@ public interface IBuffItem extends IItem { /** * Retrieve visibility increase done by the buff item. * - * @return An int, May be 0 + * @return An int, May be 0 */ default int getBuffVisibility() { return 0; } + } diff --git a/src/main/java/inf101/v18/rogue101/items/Shield.java b/src/main/java/inf101/v18/rogue101/items/buff/Shield.java similarity index 70% rename from src/main/java/inf101/v18/rogue101/items/Shield.java rename to src/main/java/inf101/v18/rogue101/items/buff/Shield.java index 49f5fac..2460448 100644 --- a/src/main/java/inf101/v18/rogue101/items/Shield.java +++ b/src/main/java/inf101/v18/rogue101/items/buff/Shield.java @@ -1,13 +1,13 @@ -package inf101.v18.rogue101.items; +package inf101.v18.rogue101.items.buff; -import inf101.v18.rogue101.game.IGame; -import inf101.v18.rogue101.objects.IItem; +import inf101.v18.rogue101.game.Game; +import inf101.v18.rogue101.object.Item; -public class Shield implements IBuffItem { +public class Shield implements BuffItem { private final int hp = getMaxHealth(); @Override - public int getBuffDefence() { + public int getBuffDefene() { return 10; } @@ -22,7 +22,7 @@ public class Shield implements IBuffItem { } @Override - public int getDefence() { + public int getDefense() { return 0; } @@ -52,7 +52,7 @@ public class Shield implements IBuffItem { } @Override - public int handleDamage(IGame game, IItem source, int amount) { + public int handleDamage(Game game, Item source, int amount) { return 0; } } diff --git a/src/main/java/inf101/v18/rogue101/items/consumable/Consumable.java b/src/main/java/inf101/v18/rogue101/items/consumable/Consumable.java new file mode 100644 index 0000000..6c3c5aa --- /dev/null +++ b/src/main/java/inf101/v18/rogue101/items/consumable/Consumable.java @@ -0,0 +1,11 @@ +package inf101.v18.rogue101.items.consumable; + +import inf101.v18.rogue101.object.Item; + +public interface Consumable extends Item { + int hpIncrease(); + + int attackIncrease(); + + int defenseIncrease(); +} diff --git a/src/main/java/inf101/v18/rogue101/items/HealthPotion.java b/src/main/java/inf101/v18/rogue101/items/consumable/HealthPotion.java similarity index 63% rename from src/main/java/inf101/v18/rogue101/items/HealthPotion.java rename to src/main/java/inf101/v18/rogue101/items/consumable/HealthPotion.java index dce93b3..8abbfb8 100644 --- a/src/main/java/inf101/v18/rogue101/items/HealthPotion.java +++ b/src/main/java/inf101/v18/rogue101/items/consumable/HealthPotion.java @@ -1,9 +1,13 @@ -package inf101.v18.rogue101.items; +package inf101.v18.rogue101.items.consumable; -import inf101.v18.rogue101.game.IGame; -import inf101.v18.rogue101.objects.IItem; +import inf101.v18.rogue101.game.Game; +import inf101.v18.rogue101.object.Item; + +/** + * A consumable that restores 100 health points + */ +public class HealthPotion implements Consumable { -public class HealthPotion implements IConsumable { @Override public int hpIncrease() { return 100; @@ -15,7 +19,7 @@ public class HealthPotion implements IConsumable { } @Override - public int defenceIncrease() { + public int defenseIncrease() { return 0; } @@ -25,7 +29,7 @@ public class HealthPotion implements IConsumable { } @Override - public int getDefence() { + public int getDefense() { return 0; } @@ -50,7 +54,8 @@ public class HealthPotion implements IConsumable { } @Override - public int handleDamage(IGame game, IItem source, int amount) { + public int handleDamage(Game game, Item source, int amount) { return 0; } + } diff --git a/src/main/java/inf101/v18/rogue101/items/Manga.java b/src/main/java/inf101/v18/rogue101/items/consumable/Manga.java similarity index 67% rename from src/main/java/inf101/v18/rogue101/items/Manga.java rename to src/main/java/inf101/v18/rogue101/items/consumable/Manga.java index 812e88f..1adec18 100644 --- a/src/main/java/inf101/v18/rogue101/items/Manga.java +++ b/src/main/java/inf101/v18/rogue101/items/consumable/Manga.java @@ -1,9 +1,13 @@ -package inf101.v18.rogue101.items; +package inf101.v18.rogue101.items.consumable; -import inf101.v18.rogue101.game.IGame; -import inf101.v18.rogue101.objects.IItem; +import inf101.v18.rogue101.game.Game; +import inf101.v18.rogue101.object.Item; + +/** + * A consumable that increases attack and defense permanently + */ +public class Manga implements Consumable { -public class Manga implements IConsumable { private int hp = getMaxHealth(); @Override @@ -12,7 +16,7 @@ public class Manga implements IConsumable { } @Override - public int getDefence() { + public int getDefense() { return 0; } @@ -42,7 +46,7 @@ public class Manga implements IConsumable { } @Override - public int handleDamage(IGame game, IItem source, int amount) { + public int handleDamage(Game game, Item source, int amount) { hp -= amount; return amount; } @@ -58,7 +62,8 @@ public class Manga implements IConsumable { } @Override - public int defenceIncrease() { + public int defenseIncrease() { return 5; } + } diff --git a/src/main/java/inf101/v18/rogue101/items/Backpack.java b/src/main/java/inf101/v18/rogue101/items/container/Backpack.java similarity index 70% rename from src/main/java/inf101/v18/rogue101/items/Backpack.java rename to src/main/java/inf101/v18/rogue101/items/container/Backpack.java index fde7ce8..4dfd724 100644 --- a/src/main/java/inf101/v18/rogue101/items/Backpack.java +++ b/src/main/java/inf101/v18/rogue101/items/container/Backpack.java @@ -1,17 +1,17 @@ -package inf101.v18.rogue101.items; +package inf101.v18.rogue101.items.container; -import inf101.v18.rogue101.game.IGame; -import inf101.v18.rogue101.objects.IItem; +import inf101.v18.rogue101.game.Game; +import inf101.v18.rogue101.object.Item; import java.util.ArrayList; import java.util.Collections; import java.util.List; -public class Backpack implements IContainer { +public class Backpack implements Container { /** * A list containing everything in the backpack. */ - private final List content = new ArrayList<>(); + private final List content = new ArrayList<>(); /** * The maximum amount of items allowed in a single backpack. */ @@ -23,7 +23,7 @@ public class Backpack implements IContainer { } @Override - public int getDefence() { + public int getDefense() { return 0; } @@ -48,7 +48,7 @@ public class Backpack implements IContainer { } @Override - public int handleDamage(IGame game, IItem source, int amount) { + public int handleDamage(Game game, Item source, int amount) { return 0; } @@ -64,7 +64,7 @@ public class Backpack implements IContainer { /** * Checks if the Backpack is empty * - * @return True if empty. False otherwise + * @return True if empty. False otherwise */ public boolean isEmpty() { return content.isEmpty(); @@ -73,10 +73,10 @@ public class Backpack implements IContainer { /** * Tries to add an item to the Backpack * - * @param item The item to add - * @return True if the item was added. False if the backpack is full + * @param item The item to add + * @return True if the item was added. False if the backpack is full */ - public boolean add(IItem item) { + public boolean add(Item item) { if (size() < MAX_SIZE) { content.add(item); return true; @@ -93,9 +93,9 @@ public class Backpack implements IContainer { /** * Removes item from the backpack. * - * @param item The item to remove + * @param item The item to remove */ - public void remove(IItem item) { + public void remove(Item item) { content.remove(item); } @@ -103,14 +103,14 @@ public class Backpack implements IContainer { * Gets a T at index i, * * @param i The index of an element - * @return An object of type T + * @return An object of type T */ - public IItem get(int i) { + public Item get(int i) { return content.get(i); } @Override - public boolean addItem(IItem item) { + public boolean addItem(Item item) { if (content.size() < MAX_SIZE) { content.add(item); return true; @@ -122,9 +122,9 @@ public class Backpack implements IContainer { /** * Gets the content List for direct manipulation. * - * @return A list of T + * @return A list of T */ - public List getContent() { + public List getContent() { return Collections.unmodifiableList(content); } @@ -134,8 +134,8 @@ public class Backpack implements IContainer { } @Override - public IItem getFirst(Class clazz) { - for (IItem item : content) { + public Item getFirst(Class clazz) { + for (Item item : content) { if (clazz.isInstance(item)) { return item; } diff --git a/src/main/java/inf101/v18/rogue101/items/Chest.java b/src/main/java/inf101/v18/rogue101/items/container/Chest.java similarity index 67% rename from src/main/java/inf101/v18/rogue101/items/Chest.java rename to src/main/java/inf101/v18/rogue101/items/container/Chest.java index 85f87fc..92a918d 100644 --- a/src/main/java/inf101/v18/rogue101/items/Chest.java +++ b/src/main/java/inf101/v18/rogue101/items/container/Chest.java @@ -1,15 +1,22 @@ -package inf101.v18.rogue101.items; +package inf101.v18.rogue101.items.container; -import inf101.v18.rogue101.game.IGame; -import inf101.v18.rogue101.objects.IItem; +import inf101.v18.rogue101.game.Game; +import inf101.v18.rogue101.items.buff.Binoculars; +import inf101.v18.rogue101.items.buff.Shield; +import inf101.v18.rogue101.items.consumable.HealthPotion; +import inf101.v18.rogue101.items.consumable.Manga; +import inf101.v18.rogue101.items.weapon.BasicBow; +import inf101.v18.rogue101.items.weapon.BasicSword; +import inf101.v18.rogue101.items.weapon.FireStaff; +import inf101.v18.rogue101.object.Item; import java.util.ArrayList; import java.util.Collections; import java.util.List; import java.util.Random; -public class Chest implements IContainer, IStatic { - private final List container; +public class Chest implements Container, Static { + private final List container; private final int MAX_SIZE = 10; public Chest() { @@ -19,15 +26,15 @@ public class Chest implements IContainer, IStatic { /** * Randomly fills chest with random items based on dungeon level. * - * @param lvl The current dungeon level + * @param lvl The current dungeon level */ - public void fill (int lvl) { + public void fill(int lvl) { Random random = new Random(); int itemChance = 3; - List items = new ArrayList<>(); - items.add(new Staff()); - items.add(new Sword()); - items.add(new Bow()); + List items = new ArrayList<>(); + items.add(new FireStaff()); + items.add(new BasicSword()); + items.add(new BasicBow()); items.add(new Binoculars()); items.add(new Shield()); items.add(new Manga()); @@ -49,12 +56,12 @@ public class Chest implements IContainer, IStatic { } @Override - public IItem get(int i) { + public Item get(int i) { return container.get(i); } @Override - public List getContent() { + public List getContent() { return Collections.unmodifiableList(container); } @@ -64,8 +71,8 @@ public class Chest implements IContainer, IStatic { } @Override - public IItem getFirst(Class clazz) { - for (IItem item : container) { + public Item getFirst(Class clazz) { + for (Item item : container) { if (clazz.isInstance(item)) { return item; } @@ -103,12 +110,12 @@ public class Chest implements IContainer, IStatic { } @Override - public int handleDamage(IGame game, IItem source, int amount) { + public int handleDamage(Game game, Item source, int amount) { return 0; } @Override - public boolean addItem(IItem item) { + public boolean addItem(Item item) { if (container.size() < MAX_SIZE) { container.add(item); return true; diff --git a/src/main/java/inf101/v18/rogue101/items/IContainer.java b/src/main/java/inf101/v18/rogue101/items/container/Container.java similarity index 83% rename from src/main/java/inf101/v18/rogue101/items/IContainer.java rename to src/main/java/inf101/v18/rogue101/items/container/Container.java index f7e7030..2fb5de5 100644 --- a/src/main/java/inf101/v18/rogue101/items/IContainer.java +++ b/src/main/java/inf101/v18/rogue101/items/container/Container.java @@ -1,10 +1,10 @@ -package inf101.v18.rogue101.items; +package inf101.v18.rogue101.items.container; -import inf101.v18.rogue101.objects.IItem; +import inf101.v18.rogue101.object.Item; import java.util.List; -public interface IContainer extends IItem { +public interface Container extends Item { /** * Retrieves an item from a container in index i * @@ -12,22 +12,22 @@ public interface IContainer extends IItem { * @return An IItem * @throws IndexOutOfBoundsException If the index is out of range. */ - IItem get(int i); + Item get(int i); /** * Adds an item to a container. * - * @return True if the container was not full * @param item The item to add to the container + * @return True if the container was not full */ - boolean addItem(IItem item); + boolean addItem(Item item); /** * Gets a list with everything inside a container. * * @return A list of Objects extending IItem */ - List getContent(); + List getContent(); /** * Checks if we can add anything at all to the container. @@ -51,7 +51,7 @@ public interface IContainer extends IItem { * @param clazz The class type to accept * @return An IItem or null */ - IItem getFirst(Class clazz); + Item getFirst(Class clazz); /** * Removes an element at index i from the container. diff --git a/src/main/java/inf101/v18/rogue101/items/container/Static.java b/src/main/java/inf101/v18/rogue101/items/container/Static.java new file mode 100644 index 0000000..2e8488f --- /dev/null +++ b/src/main/java/inf101/v18/rogue101/items/container/Static.java @@ -0,0 +1,20 @@ +package inf101.v18.rogue101.items.container; + +import inf101.v18.rogue101.object.Item; + +/** + * A static item that cannot be kept in a player's inventory + */ +public interface Static extends Item { + + @Override + default int getDefense() { + return 0; + } + + @Override + default int getSize() { + return 10000; + } + +} diff --git a/src/main/java/inf101/v18/rogue101/items/Bow.java b/src/main/java/inf101/v18/rogue101/items/weapon/BasicBow.java similarity index 76% rename from src/main/java/inf101/v18/rogue101/items/Bow.java rename to src/main/java/inf101/v18/rogue101/items/weapon/BasicBow.java index e059d51..8e197e1 100644 --- a/src/main/java/inf101/v18/rogue101/items/Bow.java +++ b/src/main/java/inf101/v18/rogue101/items/weapon/BasicBow.java @@ -1,11 +1,11 @@ -package inf101.v18.rogue101.items; +package inf101.v18.rogue101.items.weapon; -import inf101.v18.rogue101.game.IGame; -import inf101.v18.rogue101.objects.IItem; +import inf101.v18.rogue101.game.Game; +import inf101.v18.rogue101.object.Item; import java.util.Random; -public class Bow implements IRangedWeapon { +public class BasicBow implements RangedWeapon { private static final Random random = new Random(); private final int damage = 3 + random.nextInt(20); private final int hp = getMaxHealth(); @@ -26,7 +26,7 @@ public class Bow implements IRangedWeapon { } @Override - public int getDefence() { + public int getDefense() { return 0; } @@ -56,7 +56,7 @@ public class Bow implements IRangedWeapon { } @Override - public int handleDamage(IGame game, IItem source, int amount) { + public int handleDamage(Game game, Item source, int amount) { return 0; } } diff --git a/src/main/java/inf101/v18/rogue101/items/Sword.java b/src/main/java/inf101/v18/rogue101/items/weapon/BasicSword.java similarity index 76% rename from src/main/java/inf101/v18/rogue101/items/Sword.java rename to src/main/java/inf101/v18/rogue101/items/weapon/BasicSword.java index de13d03..aed557c 100644 --- a/src/main/java/inf101/v18/rogue101/items/Sword.java +++ b/src/main/java/inf101/v18/rogue101/items/weapon/BasicSword.java @@ -1,11 +1,11 @@ -package inf101.v18.rogue101.items; +package inf101.v18.rogue101.items.weapon; -import inf101.v18.rogue101.game.IGame; -import inf101.v18.rogue101.objects.IItem; +import inf101.v18.rogue101.game.Game; +import inf101.v18.rogue101.object.Item; import java.util.Random; -public class Sword implements IMeleeWeapon { +public class BasicSword implements MeleeWeapon { private static final Random random = new Random(); private final int damage = 5 + random.nextInt(25); private int hp = getMaxHealth(); @@ -26,7 +26,7 @@ public class Sword implements IMeleeWeapon { } @Override - public int getDefence() { + public int getDefense() { return 0; } @@ -56,7 +56,7 @@ public class Sword implements IMeleeWeapon { } @Override - public int handleDamage(IGame game, IItem source, int amount) { + public int handleDamage(Game game, Item source, int amount) { hp -= amount; return amount; } diff --git a/src/main/java/inf101/v18/rogue101/items/Staff.java b/src/main/java/inf101/v18/rogue101/items/weapon/FireStaff.java similarity index 77% rename from src/main/java/inf101/v18/rogue101/items/Staff.java rename to src/main/java/inf101/v18/rogue101/items/weapon/FireStaff.java index 746044a..c62c17f 100644 --- a/src/main/java/inf101/v18/rogue101/items/Staff.java +++ b/src/main/java/inf101/v18/rogue101/items/weapon/FireStaff.java @@ -1,11 +1,11 @@ -package inf101.v18.rogue101.items; +package inf101.v18.rogue101.items.weapon; -import inf101.v18.rogue101.game.IGame; -import inf101.v18.rogue101.objects.IItem; +import inf101.v18.rogue101.game.Game; +import inf101.v18.rogue101.object.Item; import java.util.Random; -public class Staff implements IMagicWeapon { +public class FireStaff implements MagicWeapon { private static final Random random = new Random(); private final int damage = 5 + random.nextInt(25); private int hp = getMaxHealth(); @@ -26,7 +26,7 @@ public class Staff implements IMagicWeapon { } @Override - public int getDefence() { + public int getDefense() { return 0; } @@ -56,7 +56,7 @@ public class Staff implements IMagicWeapon { } @Override - public int handleDamage(IGame game, IItem source, int amount) { + public int handleDamage(Game game, Item source, int amount) { hp -= amount; return amount; } diff --git a/src/main/java/inf101/v18/rogue101/items/weapon/MagicWeapon.java b/src/main/java/inf101/v18/rogue101/items/weapon/MagicWeapon.java new file mode 100644 index 0000000..6a673e7 --- /dev/null +++ b/src/main/java/inf101/v18/rogue101/items/weapon/MagicWeapon.java @@ -0,0 +1,15 @@ +package inf101.v18.rogue101.items.weapon; + +import inf101.v18.rogue101.state.Sound; + +/** + * An interface to extinguish different weapons for specific NPC. + */ +public interface MagicWeapon extends Weapon { + + @Override + default Sound getSound() { + return Sound.RANGED_STAFF; + } + +} diff --git a/src/main/java/inf101/v18/rogue101/items/weapon/MeleeWeapon.java b/src/main/java/inf101/v18/rogue101/items/weapon/MeleeWeapon.java new file mode 100644 index 0000000..6b5a996 --- /dev/null +++ b/src/main/java/inf101/v18/rogue101/items/weapon/MeleeWeapon.java @@ -0,0 +1,15 @@ +package inf101.v18.rogue101.items.weapon; + +import inf101.v18.rogue101.state.Sound; + +/** + * An interface to extinguish different weapons for specific NPC. + */ +public interface MeleeWeapon extends Weapon { + + @Override + default Sound getSound() { + return Sound.MELEE_SWORD; + } + +} diff --git a/src/main/java/inf101/v18/rogue101/items/weapon/RangedWeapon.java b/src/main/java/inf101/v18/rogue101/items/weapon/RangedWeapon.java new file mode 100644 index 0000000..e12bed4 --- /dev/null +++ b/src/main/java/inf101/v18/rogue101/items/weapon/RangedWeapon.java @@ -0,0 +1,14 @@ +package inf101.v18.rogue101.items.weapon; + +import inf101.v18.rogue101.state.Sound; + +/** + * An interface to extinguish different weapons for specific NPC. + */ +public interface RangedWeapon extends Weapon { + + @Override + default Sound getSound() { + return Sound.RANGED_BOW; + } +} diff --git a/src/main/java/inf101/v18/rogue101/items/weapon/Weapon.java b/src/main/java/inf101/v18/rogue101/items/weapon/Weapon.java new file mode 100644 index 0000000..4869557 --- /dev/null +++ b/src/main/java/inf101/v18/rogue101/items/weapon/Weapon.java @@ -0,0 +1,29 @@ +package inf101.v18.rogue101.items.weapon; + +import inf101.v18.rogue101.object.Item; +import inf101.v18.rogue101.state.Sound; + +public interface Weapon extends Item { + + /** + * Retrieves the damage points of a weapon. + * + * @return An int + */ + int getWeaponDamage(); + + /** + * Returns the attack points of a weapon + * + * @return

    The attack points as an integer

    + */ + int getWeaponAttack(); + + /** + * Retrieves the sound to play upon an attack + * + * @return

    The sound to use

    + */ + Sound getSound(); + +} diff --git a/src/main/java/inf101/v18/rogue101/map/AGameMap.java b/src/main/java/inf101/v18/rogue101/map/AGameMap.java new file mode 100644 index 0000000..4bd5af6 --- /dev/null +++ b/src/main/java/inf101/v18/rogue101/map/AGameMap.java @@ -0,0 +1,446 @@ +package inf101.v18.rogue101.map; + +import inf101.v18.gfx.gfxmode.ITurtle; +import inf101.v18.gfx.textmode.Printer; +import inf101.v18.grid.GridDirection; +import inf101.v18.grid.IArea; +import inf101.v18.grid.IMultiGrid; +import inf101.v18.grid.Location; +import inf101.v18.grid.MultiGrid; +import inf101.v18.rogue101.Main; +import inf101.v18.rogue101.examples.Carrot; +import inf101.v18.rogue101.game.IllegalMoveException; +import inf101.v18.rogue101.items.buff.Shield; +import inf101.v18.rogue101.items.consumable.Manga; +import inf101.v18.rogue101.items.container.Backpack; +import inf101.v18.rogue101.items.container.Chest; +import inf101.v18.rogue101.items.weapon.BasicBow; +import inf101.v18.rogue101.items.weapon.BasicSword; +import inf101.v18.rogue101.items.weapon.FireStaff; +import inf101.v18.rogue101.object.Actor; +import inf101.v18.rogue101.object.FakeWall; +import inf101.v18.rogue101.object.Item; +import inf101.v18.rogue101.object.PlayerCharacter; +import inf101.v18.rogue101.object.Wall; +import javafx.scene.canvas.GraphicsContext; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashSet; +import java.util.IdentityHashMap; +import java.util.LinkedList; +import java.util.List; +import java.util.Map; +import java.util.Queue; +import java.util.Random; +import java.util.Set; + +public class AGameMap implements GameMap { + /** + * The grid that makes up our map + */ + private final IMultiGrid grid; + /** + * These locations have changed, and need to be redrawn + */ + private final Set dirtyLocs = new HashSet<>(); + /** + * An index of all the items in the map and their locations. + */ + // an IdentityHashMap uses object identity as a lookup key, so items are + // considered equal if they are the same object (a == b) + private final Map items = new IdentityHashMap<>(); + + public AGameMap(IArea area) { + grid = new MultiGrid<>(area); + } + + public AGameMap(int width, int height) { + grid = new MultiGrid<>(width, height); + } + + @Override + public void add(Location loc, Item item) { + + // keep track of location of all items + items.put(item, loc); + // also keep track of whether we need to redraw this cell + dirtyLocs.add(loc); + + // do the actual adding + List list = grid.get(loc); + + for (int i = 0; i < list.size(); i++) { + if (item.compareTo(list.get(i)) >= 0) { + list.add(i, item); + return; + } + } + list.add(item); + } + + public void addRandomItems(Location loc) { + if (!this.hasActors(loc) && !this.hasWall(loc)) { + Random random = new Random(); + if (random.nextInt(10) < 2) { + this.add(loc, new Carrot()); + } + if (random.nextInt(10) < 1) { + this.add(loc, new Manga()); + } + if (random.nextInt(2) < 1) { + this.add(loc, new Chest()); + } + if (random.nextInt(2) < 1) { + this.add(loc, new BasicSword()); + } + if (random.nextInt(2) < 1) { + this.add(loc, new Shield()); + } + if (random.nextInt(2) < 1) { + this.add(loc, new FireStaff()); + } + if (random.nextInt(2) < 1) { + this.add(loc, new BasicBow()); + } + if (random.nextInt(2) < 1) { + this.add(loc, new Backpack()); + } + } + } + + @Override + public boolean canGo(Location to) { + return !grid.contains(to, (i) -> ((i instanceof Wall && !(i instanceof FakeWall)) || i instanceof Actor)); + } + + @Override + public boolean hasNeighbour(Location from, GridDirection dir) { + return from.canGo(dir); + } + + @Override + public boolean canGo(Location from, GridDirection dir) { + if (!from.canGo(dir)) { + return false; + } + Location loc = from.go(dir); + return canGo(loc); + } + + @Override + public void draw(ITurtle painter, Printer printer) { + Iterable cells; + if (Main.MAP_DRAW_ONLY_DIRTY_CELLS) { + if (dirtyLocs.isEmpty()) { + return; + } else { + cells = dirtyLocs; + } + } else { + cells = grid.locations(); + painter.as(GraphicsContext.class).clearRect(0, 0, getWidth() * printer.getCharWidth(), + getHeight() * printer.getCharHeight()); + printer.clearRegion(1, 1, getWidth(), getHeight()); + } + GraphicsContext ctx = painter.as(GraphicsContext.class); + double h = printer.getCharHeight(); + double w = printer.getCharWidth(); + if (Main.MAP_AUTO_SCALE_ITEM_DRAW) { + ctx.save(); + ctx.scale(w / h, 1.0); + w = h; + } + try { + for (Location loc : cells) { + List list = grid.get(loc); + String sym = " "; + if (!list.isEmpty()) { + if (Main.MAP_DRAW_ONLY_DIRTY_CELLS) { + ctx.clearRect(loc.getX() * w, loc.getY() * h, w, h); + // ctx.fillRect(loc.getX() * w, loc.getY() * h, w, h); + } + painter.save(); + painter.jumpTo((loc.getX() + 0.5) * w, (loc.getY() + 0.5) * h); + boolean dontPrint = list.get(0).draw(painter, w, h); + painter.restore(); + if (!dontPrint) { + sym = list.get(0).getPrintSymbol(); + } + } + printer.printAt(loc.getX() + 1, loc.getY() + 1, sym); + } + } finally { + if (Main.MAP_AUTO_SCALE_ITEM_DRAW) { + ctx.restore(); + } + } + dirtyLocs.clear(); + } + + /** + * Writes a black void unto the whole map. + * Properly draws only the tiles visible to the player. + * + * @param painter A painter + * @param printer A printer + */ + public void drawVisible(ITurtle painter, Printer printer) { + Iterable cells; + if (Main.MAP_DRAW_ONLY_DIRTY_CELLS) { + if (dirtyLocs.isEmpty()) { + return; + } else { + cells = dirtyLocs; + } + } else { + cells = grid.locations(); + painter.as(GraphicsContext.class).clearRect(0, 0, getWidth() * printer.getCharWidth(), + getHeight() * printer.getCharHeight()); + printer.clearRegion(1, 1, getWidth(), getHeight()); + } + GraphicsContext ctx = painter.as(GraphicsContext.class); + double h = printer.getCharHeight(); + double w = printer.getCharWidth(); + if (Main.MAP_AUTO_SCALE_ITEM_DRAW) { + ctx.save(); + ctx.scale(w / h, 1.0); + w = h; + } + try { + PlayerCharacter player = null; + Location playerPos = null; + for (Location loc : cells) { + printer.printAt(loc.getX() + 1, loc.getY() + 1, " "); + //We need to get the player and its location from somewhere. + if (this.hasActors(loc) && this.getActors(loc).get(0) instanceof PlayerCharacter) { + player = (PlayerCharacter) this.getActors(loc).get(0); + playerPos = loc; + } + } + if (player == null) { + return; + } + List positions = getVisible(getNeighbourhood(playerPos, player.getVision()), playerPos); + positions.add(playerPos); + for (Location loc : positions) { + List list = grid.get(loc); + String sym = "."; + if (!list.isEmpty()) { + if (Main.MAP_DRAW_ONLY_DIRTY_CELLS) { + ctx.clearRect(loc.getX() * w, loc.getY() * h, w, h); + // ctx.fillRect(loc.getX() * w, loc.getY() * h, w, h); + } + painter.save(); + painter.jumpTo((loc.getX() + 0.5) * w, (loc.getY() + 0.5) * h); + boolean dontPrint = list.get(0).draw(painter, w, h); + painter.restore(); + if (!dontPrint) { + sym = list.get(0).getPrintSymbol(); + } + + } + printer.printAt(loc.getX() + 1, loc.getY() + 1, sym); + } + } finally { + if (Main.MAP_AUTO_SCALE_ITEM_DRAW) { + ctx.restore(); + } + } + dirtyLocs.clear(); + } + + private List getVisible(List neighbours, Location loc) { + List invalid = new ArrayList<>(); + for (Location neighbour : neighbours) { + if (!hasWall(neighbour)) { + for (Location tile : loc.gridLineTo(neighbour)) { + if (hasWall(tile)) { + invalid.add(neighbour); + break; + } + } + } + } + neighbours.removeAll(invalid); + return neighbours; + } + + @Override + public List getActors(Location loc) { + List items = new ArrayList<>(); + for (Item item : grid.get(loc)) { + if (item instanceof Actor) { + items.add((Actor) item); + } + } + + return items; + } + + @Override + public List getAll(Location loc) { + return Collections.unmodifiableList(grid.get(loc)); + } + + @Override + public List getAllModifiable(Location loc) { + dirtyLocs.add(loc); + return grid.get(loc); + } + + @Override + public void clean(Location loc) { + // remove any items that have health < 0: + if (grid.get(loc).removeIf((item) -> { + if (item.isDestroyed()) { + items.remove(item); + return true; + } else { + return false; + } + })) { + dirtyLocs.add(loc); + } + } + + @Override + public IArea getArea() { + return grid.getArea(); + } + + @Override + public int getHeight() { + return grid.getHeight(); + } + + @Override + public List getItems(Location loc) { + List items = new ArrayList<>(grid.get(loc)); + items.removeIf((i) -> i instanceof Actor); + return items; + } + + @Override + public Location getLocation(Item item) { + return items.get(item); + } + + @Override + public Location getLocation(int x, int y) { + return grid.getArea().location(x, y); + } + + @Override + public Location getNeighbour(Location from, GridDirection dir) { + if (!hasNeighbour(from, dir)) { + return null; + } else { + return from.go(dir); + } + } + + @Override + public int getWidth() { + return grid.getWidth(); + } + + @Override + public Location go(Location from, GridDirection dir) throws IllegalMoveException { + if (!from.canGo(dir)) { + throw new IllegalMoveException("Cannot move outside map!"); + } + Location loc = from.go(dir); + if (!canGo(loc)) { + throw new IllegalMoveException("Occupied!"); + } + return loc; + } + + @Override + public boolean has(Location loc, Item target) { + return grid.contains(loc, target); + } + + @Override + public boolean hasActors(Location loc) { + return grid.contains(loc, (i) -> i instanceof Actor); + } + + @Override + public boolean hasItems(Location loc) { + // true if grid cell contains an item which is not an IActor + return grid.contains(loc, (i) -> !(i instanceof Actor)); + } + + @Override + public boolean hasWall(Location loc) { + return grid.contains(loc, (i) -> i instanceof Wall); + } + + @Override + public void remove(Location loc, Item item) { + grid.remove(loc, item); + items.remove(item); + dirtyLocs.add(loc); + } + + @Override + public List getNeighbourhood(Location startLocation, int distance) { + if (distance < 0 || startLocation == null) { + throw new IllegalArgumentException(); + } else if (distance == 0) { + return new ArrayList<>(); // empty! + } + + // Run BFS to find all neighbors within the specified distance + Queue queue = new LinkedList<>(); + List explored = new ArrayList<>(); + explored.add(startLocation); + queue.add(startLocation); + + while (!queue.isEmpty()) { + Location location = queue.poll(); + + // The required distance has been reached + if (Math.abs(startLocation.getX() - location.getX()) >= distance || + Math.abs(startLocation.getY() - location.getY()) >= distance) { + break; + } + + for (GridDirection gridDirection : GridDirection.EIGHT_DIRECTIONS) { + addIfValid(queue, explored, location, gridDirection); + } + } + + // While the BFS requires the start location, it's not considered its own neighbor + explored.remove(startLocation); + return explored; + } + + /** + * Adds the specified location to the queue if valid + * + * @param queue

    The queue to add the location to

    + * @param explored

    The already explored locations

    + * @param startLocation

    The location to start at

    + * @param direction

    The direction to go

    + */ + private void addIfValid(Queue queue, List explored, Location startLocation, + GridDirection direction) { + Location location; + + // If going in the specified direction would exit the map, just return + try { + location = startLocation.go(direction); + } catch (IndexOutOfBoundsException ignored) { + return; + } + + if (!explored.contains(location) && grid.isValid(location)) { + explored.add(location); + queue.add(location); + } + } + +} diff --git a/src/main/java/inf101/v18/rogue101/map/GameMap.java b/src/main/java/inf101/v18/rogue101/map/GameMap.java index f9264f6..709e0b8 100644 --- a/src/main/java/inf101/v18/rogue101/map/GameMap.java +++ b/src/main/java/inf101/v18/rogue101/map/GameMap.java @@ -1,393 +1,48 @@ package inf101.v18.rogue101.map; -import java.util.*; - import inf101.v18.gfx.gfxmode.ITurtle; import inf101.v18.gfx.textmode.Printer; -import inf101.v18.grid.GridDirection; -import inf101.v18.grid.IArea; -import inf101.v18.grid.ILocation; -import inf101.v18.grid.IMultiGrid; -import inf101.v18.grid.MultiGrid; -import inf101.v18.rogue101.Main; -import inf101.v18.rogue101.examples.Carrot; -import inf101.v18.rogue101.game.IllegalMoveException; -import inf101.v18.rogue101.items.*; -import inf101.v18.rogue101.items.Manga; -import inf101.v18.rogue101.objects.*; -import javafx.scene.canvas.GraphicsContext; +import inf101.v18.grid.Location; +import inf101.v18.rogue101.object.Item; -public class GameMap implements IGameMap { - /** - * The grid that makes up our map - */ - private final IMultiGrid grid; - /** - * These locations have changed, and need to be redrawn - */ - private final Set dirtyLocs = new HashSet<>(); - /** - * An index of all the items in the map and their locations. - */ - // an IdentityHashMap uses object identity as a lookup key, so items are - // considered equal if they are the same object (a == b) - private final Map items = new IdentityHashMap<>(); +import java.util.List; - public GameMap(IArea area) { - grid = new MultiGrid<>(area); - } +/** + * Extra map methods that are for the game class only! + * + * @author anya + */ +public interface GameMap extends MapView { - public GameMap(int width, int height) { - grid = new MultiGrid<>(width, height); - } + /** + * Draw the map + * + * @param painter + * @param printer + */ + void draw(ITurtle painter, Printer printer); - @Override - public void add(ILocation loc, IItem item) { + /** + * Get a modifiable list of items + * + * @param loc + * @return + */ + List getAllModifiable(Location loc); - // keep track of location of all items - items.put(item, loc); - // also keep track of whether we need to redraw this cell - dirtyLocs.add(loc); + /** + * Remove any destroyed items at the given location (items where {@link Item#isDestroyed()} is true) + * + * @param loc + */ + void clean(Location loc); - // do the actual adding - List list = grid.get(loc); + /** + * Remove an item + * + * @param loc + * @param item + */ + void remove(Location loc, Item item); - for (int i = 0; i < list.size(); i++) { - if (item.compareTo(list.get(i)) >= 0) { - list.add(i, item); - return; - } - } - list.add(item); - } - - public void addRandomItems(ILocation loc) { - if (!this.hasActors(loc) && !this.hasWall(loc)) { - Random random = new Random(); - if (random.nextInt(10) < 2) { - this.add(loc, new Carrot()); - } - if (random.nextInt(10) < 1) { - this.add(loc, new Manga()); - } - if (random.nextInt(2) < 1) { - this.add(loc, new Chest()); - } - if (random.nextInt(2) < 1) { - this.add(loc, new Sword()); - } - if (random.nextInt(2) < 1) { - this.add(loc, new Shield()); - } - if (random.nextInt(2) < 1) { - this.add(loc, new Staff()); - } - if (random.nextInt(2) < 1) { - this.add(loc, new Bow()); - } - if (random.nextInt(2) < 1) { - this.add(loc, new Backpack()); - } - } - } - - @Override - public boolean canGo(ILocation to) { - return !grid.contains(to, (i) -> (i instanceof Wall || i instanceof IActor)); - } - - @Override - public boolean hasNeighbour(ILocation from, GridDirection dir) { - return from.canGo(dir); - } - - @Override - public boolean canGo(ILocation from, GridDirection dir) { - if (!from.canGo(dir)) - return false; - ILocation loc = from.go(dir); - return canGo(loc); - } - - @Override - public void draw(ITurtle painter, Printer printer) { - Iterable cells; - if (Main.MAP_DRAW_ONLY_DIRTY_CELLS) { - if (dirtyLocs.isEmpty()) - return; - else - cells = dirtyLocs; - } else { - cells = grid.locations(); - painter.as(GraphicsContext.class).clearRect(0, 0, getWidth() * printer.getCharWidth(), - getHeight() * printer.getCharHeight()); - printer.clearRegion(1, 1, getWidth(), getHeight()); - } - GraphicsContext ctx = painter.as(GraphicsContext.class); - double h = printer.getCharHeight(); - double w = printer.getCharWidth(); - if (Main.MAP_AUTO_SCALE_ITEM_DRAW) { - ctx.save(); - ctx.scale(w / h, 1.0); - w = h; - } - try { - for (ILocation loc : cells) { - List list = grid.get(loc); - String sym = " "; - if (!list.isEmpty()) { - if (Main.MAP_DRAW_ONLY_DIRTY_CELLS) { - ctx.clearRect(loc.getX() * w, loc.getY() * h, w, h); - // ctx.fillRect(loc.getX() * w, loc.getY() * h, w, h); - } - painter.save(); - painter.jumpTo((loc.getX() + 0.5) * w, (loc.getY() + 0.5) * h); - boolean dontPrint = list.get(0).draw(painter, w, h); - painter.restore(); - if (!dontPrint) { - sym = list.get(0).getPrintSymbol(); - } - } - printer.printAt(loc.getX() + 1, loc.getY() + 1, sym); - } - } finally { - if (Main.MAP_AUTO_SCALE_ITEM_DRAW) { - ctx.restore(); - } - } - dirtyLocs.clear(); - } - - /** - * Writes a black void unto the whole map. - * Properly draws only the tiles visible to the player. - * - * @param painter A painter - * @param printer A printer - */ - public void drawVisible(ITurtle painter, Printer printer) { - Iterable cells; - if (Main.MAP_DRAW_ONLY_DIRTY_CELLS) { - if (dirtyLocs.isEmpty()) - return; - else - cells = dirtyLocs; - } else { - cells = grid.locations(); - painter.as(GraphicsContext.class).clearRect(0, 0, getWidth() * printer.getCharWidth(), - getHeight() * printer.getCharHeight()); - printer.clearRegion(1, 1, getWidth(), getHeight()); - } - GraphicsContext ctx = painter.as(GraphicsContext.class); - double h = printer.getCharHeight(); - double w = printer.getCharWidth(); - if (Main.MAP_AUTO_SCALE_ITEM_DRAW) { - ctx.save(); - ctx.scale(w / h, 1.0); - w = h; - } - try { - IPlayer player = null; - ILocation playerPos = null; - for (ILocation loc : cells) { - printer.printAt(loc.getX() + 1, loc.getY() + 1, " "); - //We need to get the player and its location from somewhere. - if (this.hasActors(loc) && this.getActors(loc).get(0) instanceof IPlayer) { - player = (IPlayer) this.getActors(loc).get(0); - playerPos = loc; - } - } - if (player == null) { - return; - } - List positions = getVisible(getNeighbourhood(playerPos,player.getVision()), playerPos); - positions.add(playerPos); - for (ILocation loc : positions) { - List list = grid.get(loc); - String sym = "."; - if (!list.isEmpty()) { - if (Main.MAP_DRAW_ONLY_DIRTY_CELLS) { - ctx.clearRect(loc.getX() * w, loc.getY() * h, w, h); - // ctx.fillRect(loc.getX() * w, loc.getY() * h, w, h); - } - painter.save(); - painter.jumpTo((loc.getX() + 0.5) * w, (loc.getY() + 0.5) * h); - boolean dontPrint = list.get(0).draw(painter, w, h); - painter.restore(); - if (!dontPrint) { - sym = list.get(0).getPrintSymbol(); - } - - } - printer.printAt(loc.getX() + 1, loc.getY() + 1, sym); - } - } finally { - if (Main.MAP_AUTO_SCALE_ITEM_DRAW) { - ctx.restore(); - } - } - dirtyLocs.clear(); - } - - private List getVisible(List neighbours, ILocation loc) { - List invalid = new ArrayList<>(); - for (ILocation neighbour : neighbours) { - if (!hasWall(neighbour)) { - for (ILocation tile : loc.gridLineTo(neighbour)) { - if (hasWall(tile)) { - invalid.add(neighbour); - break; - } - } - } - } - neighbours.removeAll(invalid); - return neighbours; - } - - @Override - public List getActors(ILocation loc) { - List items = new ArrayList<>(); - for (IItem item : grid.get(loc)) { - if (item instanceof IActor) - items.add((IActor) item); - } - - return items; - } - - @Override - public List getAll(ILocation loc) { - return Collections.unmodifiableList(grid.get(loc)); - } - - @Override - public List getAllModifiable(ILocation loc) { - dirtyLocs.add(loc); - return grid.get(loc); - } - - @Override - public void clean(ILocation loc) { - // remove any items that have health < 0: - if (grid.get(loc).removeIf((item) -> { - if (item.isDestroyed()) { - items.remove(item); - return true; - } else { - return false; - } - })) { - dirtyLocs.add(loc); - } - } - - @Override - public IArea getArea() { - return grid.getArea(); - } - - @Override - public int getHeight() { - return grid.getHeight(); - } - - @Override - public List getItems(ILocation loc) { - List items = new ArrayList<>(grid.get(loc)); - items.removeIf((i) -> i instanceof IActor); - return items; - } - - @Override - public ILocation getLocation(IItem item) { - return items.get(item); - } - - @Override - public ILocation getLocation(int x, int y) { - return grid.getArea().location(x, y); - } - - @Override - public ILocation getNeighbour(ILocation from, GridDirection dir) { - if (!hasNeighbour(from, dir)) - return null; - else - return from.go(dir); - } - - @Override - public int getWidth() { - return grid.getWidth(); - } - - @Override - public ILocation go(ILocation from, GridDirection dir) throws IllegalMoveException { - if (!from.canGo(dir)) - throw new IllegalMoveException("Cannot move outside map!"); - ILocation loc = from.go(dir); - if (!canGo(loc)) - throw new IllegalMoveException("Occupied!"); - return loc; - } - - @Override - public boolean has(ILocation loc, IItem target) { - return grid.contains(loc, target); - } - - @Override - public boolean hasActors(ILocation loc) { - return grid.contains(loc, (i) -> i instanceof IActor); - } - - @Override - public boolean hasItems(ILocation loc) { - // true if grid cell contains an item which is not an IActor - return grid.contains(loc, (i) -> !(i instanceof IActor)); - } - - @Override - public boolean hasWall(ILocation loc) { - return grid.contains(loc, (i) -> i instanceof Wall || i instanceof FakeWall); - } - - @Override - public void remove(ILocation loc, IItem item) { - grid.remove(loc, item); - items.remove(item); - dirtyLocs.add(loc); - } - - @Override - public List getNeighbourhood(ILocation loc, int dist) { - if (dist < 0 || loc == null) { - throw new IllegalArgumentException(); - } else if (dist == 0) { - return new ArrayList<>(); // empty! - } - List neighbours = new ArrayList<>(); - int startX = loc.getX(); - int startY = loc.getY(); - for (int i = 1; i <= dist; i++) { - for (int x = startX - i + 1; x < startX + i; x++) { //Top and bottom - if (grid.isValid(x, startY - i)) { - neighbours.add(getLocation(x, startY - i)); - } - if (grid.isValid(x, startY + i)) { - neighbours.add(getLocation(x, startY + i)); - } - } - for (int y = startY - i; y <= startY + i; y++) { //Sides - if (grid.isValid(startX - i, y)) { - neighbours.add(getLocation(startX - i, y)); - } - if (grid.isValid(startX + i, y)) { - neighbours.add(getLocation(startX + i, y)); - } - } - } - return neighbours; - } } diff --git a/src/main/java/inf101/v18/rogue101/map/IGameMap.java b/src/main/java/inf101/v18/rogue101/map/IGameMap.java deleted file mode 100644 index ebc470d..0000000 --- a/src/main/java/inf101/v18/rogue101/map/IGameMap.java +++ /dev/null @@ -1,48 +0,0 @@ -package inf101.v18.rogue101.map; - -import java.util.List; - -import inf101.v18.gfx.gfxmode.ITurtle; -import inf101.v18.gfx.textmode.Printer; -import inf101.v18.grid.ILocation; -import inf101.v18.rogue101.objects.IItem; - -/** - * Extra map methods that are for the game class only! - * - * @author anya - * - */ -public interface IGameMap extends IMapView { - - /** - * Draw the map - * - * @param painter - * @param printer - */ - void draw(ITurtle painter, Printer printer); - - /** - * Get a modifiable list of items - * - * @param loc - * @return - */ - List getAllModifiable(ILocation loc); - - /** - * Remove any destroyed items at the given location (items where {@link IItem#isDestroyed()} is true) - * - * @param loc - */ - void clean(ILocation loc); - - /** - * Remove an item - * @param loc - * @param item - */ - void remove(ILocation loc, IItem item); - -} diff --git a/src/main/java/inf101/v18/rogue101/map/IMapView.java b/src/main/java/inf101/v18/rogue101/map/IMapView.java deleted file mode 100644 index 983ac7d..0000000 --- a/src/main/java/inf101/v18/rogue101/map/IMapView.java +++ /dev/null @@ -1,190 +0,0 @@ -package inf101.v18.rogue101.map; - -import java.util.List; - -import inf101.v18.grid.GridDirection; -import inf101.v18.grid.IArea; -import inf101.v18.grid.ILocation; -import inf101.v18.rogue101.game.IllegalMoveException; -import inf101.v18.rogue101.objects.IActor; -import inf101.v18.rogue101.objects.IItem; - -public interface IMapView { - /** - * Add an item to the map. - * - * @param loc - * A location - * @param item - * the item - */ - void add(ILocation loc, IItem item); - - /** - * Check if it's legal for an IActor to go into the given location - * - * @param to - * A location - * @return True if the location isn't already occupied - */ - boolean canGo(ILocation to); - - /** - * Check if it's legal for an IActor to go in the given direction from the given - * location - * - * @param from - * Current location - * @param dir - * Direction we want to move in - * @return True if the next location exists and isn't occupied - */ - boolean canGo(ILocation from, GridDirection dir); - - /** - * Get all IActors at the given location - *

    - * The returned list either can't be modified, or modifying it won't affect the - * map. - * - * @param loc - * @return A list of actors - */ - List getActors(ILocation loc); - - /** - * Get all items (both IActors and other IItems) at the given location - *

    - * The returned list either can't be modified, or modifying it won't affect the - * map. - * - * @param loc - * @return A list of items - */ - List getAll(ILocation loc); - - /** - * Get all non-IActor items at the given location - *

    - * The returned list either can't be modified, or modifying it won't affect the - * map. - * - * @param loc - * @return A list of items, non of which are instanceof IActor - */ - List getItems(ILocation loc); - - /** - * @return A 2D-area defining the legal map locations - */ - IArea getArea(); - - /** - * @return Height of the map, same as - * {@link #getArea()}.{@link IArea#getHeight()} - */ - int getHeight(); - - /** - * @return Width of the map, same as {@link #getArea()}.{@link IArea#getWidth()} - */ - int getWidth(); - - /** - * Find location of an item - * - * @param item - * The item - * @return It's location, or null if it's not on the map - */ - ILocation getLocation(IItem item); - - /** - * Translate (x,y)-coordinates to ILocation - * - * @param x - * @param y - * @return an ILocation - * @throws IndexOutOfBoundsException - * if (x,y) is outside {@link #getArea()} - */ - ILocation getLocation(int x, int y); - - /** - * Get the neighbouring location in the given direction - * - * @param from - * A location - * @param dir - * the Direction - * @return from's neighbour in direction dir, or null, if this would be outside - * the map - */ - ILocation getNeighbour(ILocation from, GridDirection dir); - - /** - * Compute new location of an IActor moving the given direction - * - * @param from - * Original location - * @param dir - * Direction we're moving in - * @return The new location - * @throws IllegalMoveException - * if !{@link #canGo(ILocation, GridDirection)} - */ - ILocation go(ILocation from, GridDirection dir) throws IllegalMoveException; - - /** - * Check if an item exists at a location - * - * @param loc - * The location - * @param target - * The item we're interested in - * @return True if target would appear in {@link #getAll(loc)} - */ - boolean has(ILocation loc, IItem target); - - /** - * Check for actors. - * - * @param loc - * @return True if {@link #getActors(loc)} would be non-empty - */ - boolean hasActors(ILocation loc); - - /** - * Check for non-actors - * - * @param loc - * @return True if {@link #getItem(loc)} would be non-empty - */ - boolean hasItems(ILocation loc); - - /** - * Check for walls - * - * @param loc - * @return True if there is a wall at the given location ({@link #getAll(loc)} - * would contain an instance of {@link Wall}) - */ - boolean hasWall(ILocation loc); - - /** - * Check if a neighbour exists on the map - * - * @param from A location - * @param dir A direction - * @return True if {@link #getNeighbour(from, dir)} would return non-null - */ - boolean hasNeighbour(ILocation from, GridDirection dir); - - /** - * Get all locations within i steps from the given centre - * @param centre - * @param dist - * @return A list of locations, all at most i grid cells away from centre - */ - List getNeighbourhood(ILocation centre, int dist); -} diff --git a/src/main/java/inf101/v18/rogue101/map/MapReader.java b/src/main/java/inf101/v18/rogue101/map/MapReader.java index 68b0d51..15b43d7 100644 --- a/src/main/java/inf101/v18/rogue101/map/MapReader.java +++ b/src/main/java/inf101/v18/rogue101/map/MapReader.java @@ -1,23 +1,23 @@ package inf101.v18.rogue101.map; +import inf101.v18.grid.IGrid; +import inf101.v18.grid.MyGrid; + import java.io.IOException; import java.io.InputStream; import java.util.Scanner; -import inf101.v18.grid.IGrid; -import inf101.v18.grid.MyGrid; - /** * A class to read a Boulder Dash map from a file. After the file is read, the * map is stored as an {@link #IGrid} whose entries are characters. This can * then be used to be passed to the constructor in {@link #BDMap}. - * + *

    * The first line of the file should could contain two numbers. First the width * and then the height of the map. After that follows a matrix of characters * describing which object goes where in the map. '*' for wall, ' ' for empty, * 'P' for the player, 'b' for a bug, 'r' for a rock, and '#' for sand. For * example - * + * *

      * {@code
      * 5 6
    @@ -26,75 +26,77 @@ import inf101.v18.grid.MyGrid;
      * *####
      * *   *
      * *   d
    - *    b 
    + *    b
      * }
      * 
    - * + * * @author larsjaffke (original boulderdash version, 2017) * @author anya (Rogue101 update, 2018) */ public class MapReader { - /** - * This method fills the previously initialized {@link #symbolMap} with the - * characters read from the file. - */ - private static void fillMap(IGrid symbolMap, Scanner in) { - // we need to store x and y in an object (array) rather than as variables, - // otherwise the foreach and lambda below won't work. - int[] xy = new int[2]; // xy[0] is x, xy[1] is y - xy[1] = 0; - while (in.hasNextLine()) { - xy[0] = 0; - in.nextLine().codePoints().forEach((codePoint) -> { - if (xy[0] < symbolMap.getWidth()) - symbolMap.set(xy[0]++, xy[1], String.valueOf(Character.toChars(codePoint))); - }); - xy[1]++; - } - } + /** + * This method fills the previously initialized {@link #symbolMap} with the + * characters read from the file. + */ + private static void fillMap(IGrid symbolMap, Scanner in) { + // we need to store x and y in an object (array) rather than as variables, + // otherwise the foreach and lambda below won't work. + int[] xy = new int[2]; // xy[0] is x, xy[1] is y + xy[1] = 0; + while (in.hasNextLine()) { + xy[0] = 0; + in.nextLine().codePoints().forEach((codePoint) -> { + if (xy[0] < symbolMap.getWidth()) { + symbolMap.set(xy[0]++, xy[1], String.valueOf(Character.toChars(codePoint))); + } + }); + xy[1]++; + } + } - /** - * Load map from file. - *

    - * Files are search for relative to the folder containing the MapReader class. - * - * @return the dungeon map as a grid of characters read from the file, or null - * if it failed - */ - public static IGrid readFile(String path) { - IGrid symbolMap = null; - ClassLoader classloader = Thread.currentThread().getContextClassLoader(); - InputStream stream = classloader.getResourceAsStream(path); - if (stream == null) - return null; - try (Scanner in = new Scanner(stream, "UTF-8")) { - int width = in.nextInt(); - int height = in.nextInt(); - // System.out.println(width + " " + height); - symbolMap = new MyGrid(width, height, " "); - in.nextLine(); - fillMap(symbolMap, in); - } - try { - stream.close(); - } catch (IOException e) { - } - return symbolMap; - } + /** + * Load map from file. + *

    + * Files are search for relative to the folder containing the MapReader class. + * + * @return the dungeon map as a grid of characters read from the file, or null + * if it failed + */ + public static IGrid readFile(String path) { + IGrid symbolMap = null; + ClassLoader classloader = Thread.currentThread().getContextClassLoader(); + InputStream stream = classloader.getResourceAsStream(path); + if (stream == null) { + return null; + } + try (Scanner in = new Scanner(stream, "UTF-8")) { + int width = in.nextInt(); + int height = in.nextInt(); + // System.out.println(width + " " + height); + symbolMap = new MyGrid(width, height, " "); + in.nextLine(); + fillMap(symbolMap, in); + } + try { + stream.close(); + } catch (IOException e) { + } + return symbolMap; + } - /** - * @return the dungeon map as a grid of characters read from the input string, - * or null if it failed - */ - public static IGrid readString(String input) { - IGrid symbolMap = null; - try (Scanner in = new Scanner(input)) { - int width = in.nextInt(); - int height = in.nextInt(); - symbolMap = new MyGrid(width, height, " "); - in.nextLine(); - fillMap(symbolMap, in); - } - return symbolMap; - } + /** + * @return the dungeon map as a grid of characters read from the input string, + * or null if it failed + */ + public static IGrid readString(String input) { + IGrid symbolMap = null; + try (Scanner in = new Scanner(input)) { + int width = in.nextInt(); + int height = in.nextInt(); + symbolMap = new MyGrid(width, height, " "); + in.nextLine(); + fillMap(symbolMap, in); + } + return symbolMap; + } } diff --git a/src/main/java/inf101/v18/rogue101/map/MapView.java b/src/main/java/inf101/v18/rogue101/map/MapView.java new file mode 100644 index 0000000..781e124 --- /dev/null +++ b/src/main/java/inf101/v18/rogue101/map/MapView.java @@ -0,0 +1,177 @@ +package inf101.v18.rogue101.map; + +import inf101.v18.grid.GridDirection; +import inf101.v18.grid.IArea; +import inf101.v18.grid.Location; +import inf101.v18.rogue101.game.IllegalMoveException; +import inf101.v18.rogue101.object.Actor; +import inf101.v18.rogue101.object.Item; + +import java.util.List; + +public interface MapView { + /** + * Add an item to the map. + * + * @param loc A location + * @param item the item + */ + void add(Location loc, Item item); + + /** + * Check if it's legal for an IActor to go into the given location + * + * @param to A location + * @return True if the location isn't already occupied + */ + boolean canGo(Location to); + + /** + * Check if it's legal for an IActor to go in the given direction from the given + * location + * + * @param from Current location + * @param dir Direction we want to move in + * @return True if the next location exists and isn't occupied + */ + boolean canGo(Location from, GridDirection dir); + + /** + * Get all IActors at the given location + *

    + * The returned list either can't be modified, or modifying it won't affect the + * map. + * + * @param loc + * @return A list of actors + */ + List getActors(Location loc); + + /** + * Get all items (both IActors and other IItems) at the given location + *

    + * The returned list either can't be modified, or modifying it won't affect the + * map. + * + * @param loc + * @return A list of items + */ + List getAll(Location loc); + + /** + * Get all non-IActor items at the given location + *

    + * The returned list either can't be modified, or modifying it won't affect the + * map. + * + * @param loc + * @return A list of items, non of which are instanceof IActor + */ + List getItems(Location loc); + + /** + * @return A 2D-area defining the legal map locations + */ + IArea getArea(); + + /** + * @return Height of the map, same as + * {@link #getArea()}.{@link IArea#getHeight()} + */ + int getHeight(); + + /** + * @return Width of the map, same as {@link #getArea()}.{@link IArea#getWidth()} + */ + int getWidth(); + + /** + * Find location of an item + * + * @param item The item + * @return It's location, or null if it's not on the map + */ + Location getLocation(Item item); + + /** + * Translate (x,y)-coordinates to ILocation + * + * @param x + * @param y + * @return an ILocation + * @throws IndexOutOfBoundsException if (x,y) is outside {@link #getArea()} + */ + Location getLocation(int x, int y); + + /** + * Get the neighbouring location in the given direction + * + * @param from A location + * @param dir the Direction + * @return from's neighbour in direction dir, or null, if this would be outside + * the map + */ + Location getNeighbour(Location from, GridDirection dir); + + /** + * Compute new location of an IActor moving the given direction + * + * @param from Original location + * @param dir Direction we're moving in + * @return The new location + * @throws IllegalMoveException if !{@link #canGo(Location, GridDirection)} + */ + Location go(Location from, GridDirection dir) throws IllegalMoveException; + + /** + * Check if an item exists at a location + * + * @param loc The location + * @param target The item we're interested in + * @return True if target would appear in {@link #getAll(loc)} + */ + boolean has(Location loc, Item target); + + /** + * Check for actors. + * + * @param loc + * @return True if {@link #getActors(loc)} would be non-empty + */ + boolean hasActors(Location loc); + + /** + * Check for non-actors + * + * @param loc + * @return True if {@link #getItem(loc)} would be non-empty + */ + boolean hasItems(Location loc); + + /** + * Check for walls + * + * @param loc + * @return True if there is a wall at the given location ({@link #getAll(loc)} + * would contain an instance of {@link Wall}) + */ + boolean hasWall(Location loc); + + /** + * Check if a neighbour exists on the map + * + * @param from A location + * @param dir A direction + * @return True if {@link #getNeighbour(from, dir)} would return non-null + */ + boolean hasNeighbour(Location from, GridDirection dir); + + /** + * Get all locations within i steps from the given centre + * + * @param centre + * @param dist + * @return A list of locations, all at most i grid cells away from centre + */ + List getNeighbourhood(Location centre, int dist); +} diff --git a/src/main/java/inf101/v18/rogue101/object/Actor.java b/src/main/java/inf101/v18/rogue101/object/Actor.java new file mode 100644 index 0000000..9cbc88c --- /dev/null +++ b/src/main/java/inf101/v18/rogue101/object/Actor.java @@ -0,0 +1,43 @@ +package inf101.v18.rogue101.object; + +import inf101.v18.rogue101.game.Game; + +/** + * An actor is an Item that can also do something, either controlled by the + * computer (NonPlayerCharacter) or the user (PlayerCharacter). + * + * @author anya + */ +public interface Actor extends Item { + + /** + * @return This actor's attack score (used against an item's + * {@link #getDefense()} score to see if an attack is successful) + */ + int getAttack(); + + /** + * @return The damage this actor deals on a successful attack (used together + * with + * {@link #handleDamage(Game, Item, int)} on + * the target) + */ + int getDamage(); + + /** + * How many tiles the IActor is able to see in each direction. + * + * @return Number of tiles + */ + default int getVision() { + return 1; + } + + /** + * Gets an item of the specified type, if the actor has the item. + * + * @return An IItem or null + */ + Item getItem(Class type); + +} diff --git a/src/main/java/inf101/v18/rogue101/object/Dust.java b/src/main/java/inf101/v18/rogue101/object/Dust.java new file mode 100644 index 0000000..513e6a0 --- /dev/null +++ b/src/main/java/inf101/v18/rogue101/object/Dust.java @@ -0,0 +1,49 @@ +package inf101.v18.rogue101.object; + +import inf101.v18.gfx.gfxmode.ITurtle; +import inf101.v18.gfx.textmode.BlocksAndBoxes; +import inf101.v18.rogue101.game.Game; + +public class Dust implements Item { + + @Override + public boolean draw(ITurtle painter, double w, double h) { + return false; + } + + @Override + public int getCurrentHealth() { + return 0; + } + + @Override + public int getDefense() { + return 0; + } + + @Override + public int getMaxHealth() { + return 0; + } + + @Override + public String getName() { + return "thick layer of dust"; + } + + @Override + public int getSize() { + return 1; + } + + @Override + public String getSymbol() { + return BlocksAndBoxes.BLOCK_HALF; + } + + @Override + public int handleDamage(Game game, Item source, int amount) { + return 0; + } + +} diff --git a/src/main/java/inf101/v18/rogue101/object/FakeWall.java b/src/main/java/inf101/v18/rogue101/object/FakeWall.java new file mode 100644 index 0000000..96bcce0 --- /dev/null +++ b/src/main/java/inf101/v18/rogue101/object/FakeWall.java @@ -0,0 +1,31 @@ +package inf101.v18.rogue101.object; + +import inf101.v18.gfx.gfxmode.ITurtle; +import inf101.v18.gfx.textmode.BlocksAndBoxes; + +/** + * An objects that works like a wall in every sense, except it can be passed through + */ +public class FakeWall extends Wall { + + @Override + public boolean draw(ITurtle painter, double w, double h) { + return false; + } + + @Override + public int getMaxHealth() { + return 1; + } + + @Override + public String getName() { + return "fake wall"; + } + + @Override + public String getSymbol() { + return BlocksAndBoxes.BLOCK_HALF; + } + +} diff --git a/src/main/java/inf101/v18/rogue101/object/Item.java b/src/main/java/inf101/v18/rogue101/object/Item.java new file mode 100644 index 0000000..1cf3800 --- /dev/null +++ b/src/main/java/inf101/v18/rogue101/object/Item.java @@ -0,0 +1,175 @@ +package inf101.v18.rogue101.object; + +import inf101.v18.gfx.gfxmode.ITurtle; +import inf101.v18.rogue101.events.IEvent; +import inf101.v18.rogue101.game.Game; + +/** + * An {@link Item} is something that can be placed on the map or into a + * container of items. + *

    + * An {@link Actor} is a special case of {@link Item} used for things that are + * “alive”, such as the player or AI-controlled monsters/NPCs. + *

    + * By default, all items have a hit point / health system (they can be damaged), + * and can be picked up. So you could potentially destroy an item or pick up a + * monster. + * + * @author anya + */ +public interface Item extends Comparable { + + @Override + default int compareTo(Item other) { + return Integer.compare(getSize(), other.getSize()); + } + + /** + * Draw this item on the screen. + *

    + * The turtle-painter will be positioned in the centre of the cell. You should + * avoid drawing outside the cell (size is indicated by the w and + * h parameters, so you can move ±w/2 and ±h/2 from the initial + * position). If you want to start in the lower left corner of the cell, you can + * start by doing painter.jump(-w/2,h/2) ((0,0) is in the top-left + * corner of the screen, so negative X points left and positive Y points down). + *

    + * If this method returns true, the game will not print + * {@link #getSymbol()} in the cell. + *

    + * All calls to painter.save() must be matched by a call to + * painter.restore(). + * + * @param painter A turtle-painter for drawing + * @param w The width of the cell + * @param h The height of the cell + * @return False if the letter from {@link #getSymbol()} should be drawn instead + */ + default boolean draw(ITurtle painter, double w, double h) { + return false; + } + + /** + * @return "a" or "an", depending on the name + */ + default String getArticle() { + return "a"; + } + + /** + * Get current remaining health points. + *

    + * An object's health points determines how much damage it can take + * before it is destroyed / broken / killed. + * + * @return Current health points for this item + */ + int getCurrentHealth(); + + /** + * The defence score determines how hard an object/actor is to hit or grab. + * + * @return Defence score of this object + */ + int getDefense(); + + /** + * Get item health as a 0.0..1.0 proportion. + * + *

  • getHealth() >= 1.0 means perfect condition + *
  • getHealth() <= 0.0 means broken or dead + *
  • 0.0 < getHealth() < 1.0 means partially damaged + * + * @return Health, in the range 0.0 to 1.0 + */ + default double getHealthStatus() { + return getMaxHealth() > 0 ? getCurrentHealth() / getMaxHealth() : 0; + } + + /** + * Get maximum health points. + *

    + * An object's health points determines how much damage it can take + * before it is destroyed / broken / killed. + * + * @return Max health points for this item + */ + int getMaxHealth(); + + /** + * Get a (user-friendly) name for the item + *

    + * Used for things like "You see " + getArticle() + " " + getName() + * + * @return Item's name + */ + String getName(); + + /** + * Get a map symbol used for printing this item on the screen. + *

    + * This is usually the same as {@link #getSymbol()}, but could also include + * special control characters for changing the text colour, for example. + * + * @return A string to be displayed for this item on the screen (should be only + * one column wide when printed) + * @see ANSI + * escape code (on Wikipedia) + */ + default String getPrintSymbol() { + return getSymbol(); + } + + /** + * Get the size of the object. + *

    + * The size determines how much space an item will use if put into a container. + * + * @return Size of the item + */ + int getSize(); + + /** + * Get the map symbol of this item. + *

    + * The symbol can be used on a text-only map, or when loading a map from text. + *

    + * The symbol should be a single Unicode codepoint (i.e., + * getSymbol().codePointCount(0, getSymbol().length()) == 1). In + * most cases this means that the symbol should be a single character (i.e., + * getSymbol().length() == 1); but there are a few Unicode characters (such as + * many emojis and special symbols) that require two Java chars. + * + * @return A single-codepoint string with the item's symbol + */ + String getSymbol(); + + /** + * Inform the item that it has been damaged + * + * @param game The game + * @param source The item (usually an IActor) that caused the damage + * @param amount How much damage the item should take + * @return Amount of damage actually taken (could be less than + * amount due to armour/protection effects) + */ + int handleDamage(Game game, Item source, int amount); + + /** + * Inform the item that something has happened. + * + * @param event An object describing the event. + * @return + */ + default T handleEvent(IEvent event) { + return event.getData(); + } + + /** + * @return True if this item has been destroyed, and should be removed from the map + */ + default boolean isDestroyed() { + return getCurrentHealth() < 0; + } + +} diff --git a/src/main/java/inf101/v18/rogue101/object/NonPlayerCharacter.java b/src/main/java/inf101/v18/rogue101/object/NonPlayerCharacter.java new file mode 100644 index 0000000..c01051c --- /dev/null +++ b/src/main/java/inf101/v18/rogue101/object/NonPlayerCharacter.java @@ -0,0 +1,22 @@ +package inf101.v18.rogue101.object; + +import inf101.v18.rogue101.game.Game; + +/** + * An actor controlled by the computer + * + * @author anya + */ +public interface NonPlayerCharacter extends Actor { + + /** + * Do one turn for this non-player + *

    + * This INonPlayer will be the game's current actor ({@link Game#getActor()}) + * for the duration of this method call. + * + * @param game Game, for interacting with the world + */ + void doTurn(Game game); + +} diff --git a/src/main/java/inf101/v18/rogue101/objects/Player.java b/src/main/java/inf101/v18/rogue101/object/Player.java similarity index 64% rename from src/main/java/inf101/v18/rogue101/objects/Player.java rename to src/main/java/inf101/v18/rogue101/object/Player.java index 126fe03..4b31b2d 100644 --- a/src/main/java/inf101/v18/rogue101/objects/Player.java +++ b/src/main/java/inf101/v18/rogue101/object/Player.java @@ -1,31 +1,40 @@ -package inf101.v18.rogue101.objects; +package inf101.v18.rogue101.object; import inf101.v18.gfx.textmode.Printer; import inf101.v18.grid.GridDirection; -import inf101.v18.grid.ILocation; +import inf101.v18.grid.Location; import inf101.v18.rogue101.Main; import inf101.v18.rogue101.game.Game; -import inf101.v18.rogue101.game.IGame; -import inf101.v18.rogue101.items.*; -import inf101.v18.rogue101.shared.NPC; -import inf101.v18.rogue101.states.Attack; +import inf101.v18.rogue101.game.RogueGame; +import inf101.v18.rogue101.items.buff.BuffItem; +import inf101.v18.rogue101.items.consumable.Consumable; +import inf101.v18.rogue101.items.container.Backpack; +import inf101.v18.rogue101.items.container.Container; +import inf101.v18.rogue101.items.container.Static; +import inf101.v18.rogue101.items.weapon.MagicWeapon; +import inf101.v18.rogue101.items.weapon.RangedWeapon; +import inf101.v18.rogue101.items.weapon.Weapon; +import inf101.v18.rogue101.state.AttackType; +import inf101.v18.util.NPCHelper; import javafx.scene.input.KeyCode; + import java.util.List; -public class Player implements IPlayer { - private int hp = getMaxHealth(); +public class Player implements PlayerCharacter { + + private int healthPoints = getMaxHealth(); private int attack = 40; private int defence = 20; private final Backpack equipped = new Backpack(); private boolean dropping = false; //True if the player wants to drop something using the digit keys private boolean picking = false; //True if the player wants to pick up something using the digit keys private boolean writing = false; //True if the user is writing a name - private boolean exploringChest = false; + private boolean exploringChest = false; private String text = ""; private String name = "Player"; @Override - public boolean keyPressed(IGame game, KeyCode key) { + public boolean keyPressed(Game game, KeyCode key) { boolean turnConsumed = false; if (writing) { write(game, key); @@ -72,9 +81,9 @@ public class Player implements IPlayer { game.displayMessage("Please enter your name: "); writing = true; } else if (key == KeyCode.R) { - turnConsumed = nonMeleeAttack(game, getVision(), Attack.RANGED, "ranged"); + turnConsumed = nonMeleeAttack(game, getVision(), AttackType.RANGED, "ranged"); } else if (key == KeyCode.F) { - turnConsumed = nonMeleeAttack(game, getVision() / 2 + getVision() % 2 == 0 ? 0 : 1, Attack.MAGIC, "magic"); + turnConsumed = nonMeleeAttack(game, getVision() / 2 + getVision() % 2 == 0 ? 0 : 1, AttackType.MAGIC, "magic"); } showStatus(game); return turnConsumed; @@ -83,21 +92,21 @@ public class Player implements IPlayer { /** * Attacks if the user is able to use the desired attack. * - * @param game An IGame object - * @param range The range of the weapon - * @param type The type of the attack - * @param weapon The required weapon type - * @return True if an attack was possible. False otherwise + * @param game An IGame object + * @param range The range of the weapon + * @param type The type of the attack + * @param weapon The required weapon type + * @return True if an attack was possible. False otherwise */ - private boolean nonMeleeAttack(IGame game, int range, Attack type, String weapon) { + private boolean nonMeleeAttack(Game game, int range, AttackType type, String weapon) { boolean turnConsumed = false; - IItem item = null; + Item item = null; switch (type) { case RANGED: - item = getItem(IRangedWeapon.class); + item = getItem(RangedWeapon.class); break; case MAGIC: - item = getItem(IMagicWeapon.class); + item = getItem(MagicWeapon.class); } if (item == null) { game.displayMessage("You do not have a " + weapon + " weapon."); @@ -113,10 +122,10 @@ public class Player implements IPlayer { /** * Lets the user write his/her name. * - * @param game An IGame object - * @param key The key pressed + * @param game An IGame object + * @param key The key pressed */ - private void write(IGame game, KeyCode key) { + private void write(Game game, KeyCode key) { if (key == KeyCode.BACK_SPACE) { if (text.length() > 0) { text = text.substring(0, text.length() - 1); @@ -140,22 +149,22 @@ public class Player implements IPlayer { /** * Initializes the interaction with an item, and does what is needed. * - * @param game An IGame object - * @return True if a turn was consumed. False otherwise + * @param game An IGame object + * @return True if a turn was consumed. False otherwise */ - private boolean interactInit(IGame game) { - List items = game.getLocalItems(); + private boolean interactInit(Game game) { + List items = game.getLocalItems(); if (items.size() < 1) { game.displayMessage("There is nothing to pick up"); } else { - IItem item = items.get(0); - if (item instanceof IStatic && item instanceof IContainer) { //A Static item is always the biggest (and hopefully the only) item on a tile. + Item item = items.get(0); + if (item instanceof Static && item instanceof Container) { //A Static item is always the biggest (and hopefully the only) item on a tile. openChest(game, items); - } else if (item instanceof IStatic && item instanceof StairsUp) { - ((Game)game).goUp(); + } else if (item instanceof StairsUp) { + ((RogueGame) game).goUp(); return true; - } else if (item instanceof IStatic && item instanceof StairsDown) { - ((Game)game).goDown(); + } else if (item instanceof StairsDown) { + ((RogueGame) game).goDown(); return true; } else { if (items.size() == 1) { @@ -173,18 +182,18 @@ public class Player implements IPlayer { /** * Uses the first consumable from the player's inventory * - * @param game An IGame object - * @return True if a potion was used. False otherwise + * @param game An IGame object + * @return True if a potion was used. False otherwise */ - private boolean useConsumable(IGame game) { - IConsumable consumable = (IConsumable) equipped.getFirst(IConsumable.class); + private boolean useConsumable(Game game) { + Consumable consumable = (Consumable) equipped.getFirst(Consumable.class); if (consumable != null) { - hp += consumable.hpIncrease(); - if (hp > getMaxHealth()) { - hp = getMaxHealth(); + healthPoints += consumable.hpIncrease(); + if (healthPoints > getMaxHealth()) { + healthPoints = getMaxHealth(); } attack += consumable.attackIncrease(); - defence += consumable.defenceIncrease(); + defence += consumable.defenseIncrease(); equipped.remove(consumable); game.displayMessage("Used " + consumable.getName()); return true; @@ -200,8 +209,8 @@ public class Player implements IPlayer { * @param game An IGame object * @param items A list of items on the current tile */ - private void openChest(IGame game, List items) { - IContainer container = (IContainer) items.get(0); + private void openChest(Game game, List items) { + Container container = (Container) items.get(0); items = container.getContent(); game.displayMessage(container.getInteractMessage() + niceList(items, 10)); exploringChest = true; @@ -210,11 +219,11 @@ public class Player implements IPlayer { /** * Creates a string containing a set number of item names from a list. * - * @param list The list of items - * @param max The maximum number of items to print - * @return A string + * @param list The list of items + * @param max The maximum number of items to print + * @return A string */ - private String niceList(List list, int max) { + private String niceList(List list, int max) { StringBuilder msg = new StringBuilder(); for (int i = 0; i < Math.min(list.size(), max); i++) { msg.append(" [").append(i + 1).append("] ").append(firstCharToUpper(list.get(i).getName())); @@ -225,10 +234,10 @@ public class Player implements IPlayer { /** * Initialized the dropping of an item, and does what is needed. * - * @param game An IGame object - * @return True if a turn was consumed. False otherwise + * @param game An IGame object + * @return True if a turn was consumed. False otherwise */ - private boolean dropInit(IGame game) { + private boolean dropInit(Game game) { if (equipped.size() == 1) { drop(game, 0); return true; @@ -244,14 +253,14 @@ public class Player implements IPlayer { /** * Picks up an item at index i. * - * @param game An IGame object - * @param i The wanted index + * @param game An IGame object + * @param i The wanted index */ - private void pickUp(IGame game, int i) { + private void pickUp(Game game, int i) { if (equipped.hasSpace()) { - List items = game.getLocalItems(); + List items = game.getLocalItems(); if (items.size() >= i) { - IItem pickedUp = game.pickUp(items.get(i)); + Item pickedUp = game.pickUp(items.get(i)); if (pickedUp != null) { equipped.add(pickedUp); game.displayMessage("Picked up " + pickedUp.getName()); @@ -269,11 +278,11 @@ public class Player implements IPlayer { /** * Takes an item from a chest, and gives it to the player. */ - private void loot(IGame game, int i) { + private void loot(Game game, int i) { if (equipped.hasSpace()) { - IContainer container = getStaticContainer(game); + Container container = getStaticContainer(game); if (container != null && i < container.getContent().size()) { - IItem loot = container.getContent().get(i); + Item loot = container.getContent().get(i); equipped.add(loot); container.remove(i); game.displayMessage("Looted " + loot.getName()); @@ -286,17 +295,17 @@ public class Player implements IPlayer { /** * Gets any static containers on the current tile. * - * @param game An IGame object - * @return A static container + * @param game An IGame object + * @return A static container */ - private IContainer getStaticContainer(IGame game) { - List items = game.getLocalItems(); + private Container getStaticContainer(Game game) { + List items = game.getLocalItems(); if (items.size() < 1) { return null; } - IItem item = items.get(0); - if (item instanceof IStatic && item instanceof IContainer) { - return (IContainer) item; + Item item = items.get(0); + if (item instanceof Static && item instanceof Container) { + return (Container) item; } else { return null; } @@ -305,12 +314,12 @@ public class Player implements IPlayer { /** * Drops an item at index i. * - * @param game An IGame object - * @param i The wanted index + * @param game An IGame object + * @param i The wanted index */ - private void drop(IGame game, int i) { + private void drop(Game game, int i) { if (!equipped.isEmpty() && equipped.size() > i) { - IContainer container = getStaticContainer(game); + Container container = getStaticContainer(game); if (container != null) { if (container.addItem(equipped.get(i))) { game.displayMessage(equipped.get(i).getName() + " stored in " + container.getName()); @@ -335,18 +344,18 @@ public class Player implements IPlayer { /** * Updates the status bar with the Player's current stats. * - * @param game An IGame object + * @param game An IGame object */ - private void showStatus(IGame game) { + private void showStatus(Game game) { game.formatStatus("HP: %s ATK: %d RATK: %d MATK: %d DEF: %s DMG: %s RDMG: %s MDMG: %s", - NPC.hpBar(this), - getAttack(Attack.MELEE), - getAttack(Attack.RANGED), - getAttack(Attack.MAGIC), - getDefence(), - getDamage(Attack.MELEE), - getDamage(Attack.RANGED), - getDamage(Attack.MAGIC) + NPCHelper.hpBar(this), + getAttack(AttackType.MELEE), + getAttack(AttackType.RANGED), + getAttack(AttackType.MAGIC), + getDefense(), + getDamage(AttackType.MELEE), + getDamage(AttackType.RANGED), + getDamage(AttackType.MAGIC) ); printInventory(game); } @@ -356,13 +365,13 @@ public class Player implements IPlayer { * * @param game An IGame object */ - private void printInventory(IGame game) { + private void printInventory(Game game) { Printer printer = game.getPrinter(); - List items = equipped.getContent(); - printer.clearRegion(Main.COLUMN_RIGHTSIDE_START, 13, 45, 5); - printer.printAt(Main.COLUMN_RIGHTSIDE_START, 12, "Inventory:"); + List items = equipped.getContent(); + printer.clearRegion(Main.COLUMN_RIGHT_SIDE_START, 13, 45, 5); + printer.printAt(Main.COLUMN_RIGHT_SIDE_START, 12, "Inventory:"); for (int i = 0; i < items.size(); i++) { - printer.printAt(Main.COLUMN_RIGHTSIDE_START, 13 + i, items.get(i).getName()); + printer.printAt(Main.COLUMN_RIGHT_SIDE_START, 13 + i, items.get(i).getName()); } } @@ -370,11 +379,11 @@ public class Player implements IPlayer { * Changes the first character in a string to uppercase. * * @param input The input string - * @return The input string with the first character uppercased + * @return The input string with the first character uppercased */ private String firstCharToUpper(String input) { if (input.length() < 1) { - return input; + return input; } else { return Character.toUpperCase(input.charAt(0)) + input.substring(1); } @@ -383,24 +392,24 @@ public class Player implements IPlayer { /** * Lets the user move or attack if possible. * - * @param game An IGame object - * @param dir The direction the player wants to go + * @param game An IGame object + * @param dir The direction the player wants to go */ - private void tryToMove(IGame game, GridDirection dir) { - ILocation loc = game.getLocation(); + private void tryToMove(Game game, GridDirection dir) { + Location loc = game.getLocation(); if (game.canGo(dir)) { game.move(dir); } else if (loc.canGo(dir) && game.getMap().hasActors(loc.go(dir))) { game.attack(dir, game.getMap().getActors(loc.go(dir)).get(0)); } else { - game.displayMessage("Umm, it is not possible to move in that direction."); + game.displayMessage("Umm, it is not possible to move in that direction."); } } @Override public int getAttack() { - IBuffItem buff = (IBuffItem)getItem(IBuffItem.class); + BuffItem buff = (BuffItem) getItem(BuffItem.class); if (buff != null) { return attack + buff.getBuffDamage(); } else { @@ -411,11 +420,11 @@ public class Player implements IPlayer { /** * Gets the attack with added weapon attack * - * @param type The attack type corresponding to the weapon type. - * @return Attack as int + * @param type The attack type corresponding to the weapon type. + * @return Attack as int */ - private int getAttack(Attack type) { - IWeapon weapon = NPC.getWeapon(type, this); + private int getAttack(AttackType type) { + Weapon weapon = NPCHelper.getWeapon(type, this); if (weapon != null) { return getAttack() + weapon.getWeaponAttack(); } else { @@ -431,11 +440,11 @@ public class Player implements IPlayer { /** * Gets the damage with added weapon damage * - * @param type The attack type corresponding to the weapon type. - * @return Damage as int + * @param type The attack type corresponding to the weapon type. + * @return Damage as int */ - private int getDamage(Attack type) { - IWeapon weapon = NPC.getWeapon(type, this); + private int getDamage(AttackType type) { + Weapon weapon = NPCHelper.getWeapon(type, this); if (weapon != null) { return getDamage() + weapon.getWeaponDamage(); } else { @@ -444,8 +453,8 @@ public class Player implements IPlayer { } @Override - public IItem getItem(Class type) { - for (IItem item : equipped.getContent()) { + public Item getItem(Class type) { + for (Item item : equipped.getContent()) { if (type.isInstance(item)) { return item; } @@ -455,12 +464,12 @@ public class Player implements IPlayer { @Override public int getCurrentHealth() { - return hp; + return healthPoints; } @Override - public int getDefence() { - IBuffItem buff = (IBuffItem)getItem(IBuffItem.class); + public int getDefense() { + BuffItem buff = (BuffItem) getItem(BuffItem.class); if (buff != null) { return defence + buff.getBuffDamage(); } else { @@ -485,7 +494,7 @@ public class Player implements IPlayer { @Override public String getPrintSymbol() { - return "\u001b[96m" + "@" + "\u001b[0m"; + return "\u001b[96m" + "@" + "\u001b[0m"; } @Override @@ -494,10 +503,10 @@ public class Player implements IPlayer { } @Override - public int handleDamage(IGame game, IItem source, int amount) { - hp -= amount; + public int handleDamage(Game game, Item source, int amount) { + healthPoints -= amount; showStatus(game); - if (hp < 1) { + if (healthPoints < 1) { game.displayStatus("Game Over"); } return amount; @@ -505,7 +514,7 @@ public class Player implements IPlayer { @Override public int getVision() { - IBuffItem item = (IBuffItem) getItem(IBuffItem.class); + BuffItem item = (BuffItem) getItem(BuffItem.class); if (item != null) { return 3 + item.getBuffVisibility(); } else { @@ -516,19 +525,19 @@ public class Player implements IPlayer { /** * Performs a ranged attack on the nearest visible enemy (if any); * - * @param game An IGame object - * @param range The range of the attack - * @param type The attack type - * @return True if the player attacked. False otherwise + * @param game An IGame object + * @param range The range of the attack + * @param type The attack type + * @return True if the player attacked. False otherwise */ - private boolean rangedAttack(IGame game, int range, Attack type) { - List neighbours = game.getVisible(); - for (ILocation neighbour : neighbours) { + private boolean rangedAttack(Game game, int range, AttackType type) { + List neighbours = game.getVisible(); + for (Location neighbour : neighbours) { if (game.getMap().hasActors(neighbour)) { - ILocation current = game.getLocation(); + Location current = game.getLocation(); List dirs = game.locationDirection(current, neighbour); - IActor actor = game.getMap().getActors(neighbour).get(0); //We assume there is only one actor. - if (actor instanceof INonPlayer && current.gridDistanceTo(neighbour) <= range) { + Actor actor = game.getMap().getActors(neighbour).get(0); //We assume there is only one actor. + if (actor instanceof NonPlayerCharacter && current.gridDistanceTo(neighbour) <= range) { GridDirection dir = dirs.get(0); //Only ever has one item game.rangedAttack(dir, actor, type); return true; diff --git a/src/main/java/inf101/v18/rogue101/object/PlayerCharacter.java b/src/main/java/inf101/v18/rogue101/object/PlayerCharacter.java new file mode 100644 index 0000000..23ceaff --- /dev/null +++ b/src/main/java/inf101/v18/rogue101/object/PlayerCharacter.java @@ -0,0 +1,25 @@ +package inf101.v18.rogue101.object; + +import inf101.v18.rogue101.game.Game; +import javafx.scene.input.KeyCode; + +public interface PlayerCharacter extends Actor { + /** + * Send key presses from the human player to the player object. + *

    + * The player object should interpret the key presses, and then perform its + * moves or whatever, according to the game's rules and the player's + * instructions. + *

    + * This IPlayer will be the game's current actor ({@link Game#getActor()}) and + * be at {@link Game#getLocation()}, when this method is called. + *

    + * This method may be called many times in a single turn; the turn ends + * {@link #keyPressed(Game, KeyCode)} returns and the player has used its + * movement points (e.g., by calling {@link Game#move(inf101.v18.grid.GridDirection)}). + * + * @param game Game, for interacting with the world + * @return True if the player has done anything consuming a turn. False otherwise + */ + boolean keyPressed(Game game, KeyCode key); +} diff --git a/src/main/java/inf101/v18/rogue101/objects/StairsDown.java b/src/main/java/inf101/v18/rogue101/object/StairsDown.java similarity index 58% rename from src/main/java/inf101/v18/rogue101/objects/StairsDown.java rename to src/main/java/inf101/v18/rogue101/object/StairsDown.java index df24f41..43556a3 100644 --- a/src/main/java/inf101/v18/rogue101/objects/StairsDown.java +++ b/src/main/java/inf101/v18/rogue101/object/StairsDown.java @@ -1,16 +1,20 @@ -package inf101.v18.rogue101.objects; +package inf101.v18.rogue101.object; -import inf101.v18.rogue101.game.IGame; -import inf101.v18.rogue101.items.IStatic; +import inf101.v18.rogue101.game.Game; +import inf101.v18.rogue101.items.container.Static; + +/** + * Stairs going downwards to the previous floor + */ +public class StairsDown implements Item, Static { -public class StairsDown implements IItem, IStatic { @Override public int getCurrentHealth() { return 0; } @Override - public int getDefence() { + public int getDefense() { return 0; } @@ -35,7 +39,8 @@ public class StairsDown implements IItem, IStatic { } @Override - public int handleDamage(IGame game, IItem source, int amount) { + public int handleDamage(Game game, Item source, int amount) { return 0; } + } diff --git a/src/main/java/inf101/v18/rogue101/objects/StairsUp.java b/src/main/java/inf101/v18/rogue101/object/StairsUp.java similarity index 58% rename from src/main/java/inf101/v18/rogue101/objects/StairsUp.java rename to src/main/java/inf101/v18/rogue101/object/StairsUp.java index 16bee15..c98aea7 100644 --- a/src/main/java/inf101/v18/rogue101/objects/StairsUp.java +++ b/src/main/java/inf101/v18/rogue101/object/StairsUp.java @@ -1,16 +1,20 @@ -package inf101.v18.rogue101.objects; +package inf101.v18.rogue101.object; -import inf101.v18.rogue101.game.IGame; -import inf101.v18.rogue101.items.IStatic; +import inf101.v18.rogue101.game.Game; +import inf101.v18.rogue101.items.container.Static; + +/** + * Stairs going upwards to the next floor + */ +public class StairsUp implements Item, Static { -public class StairsUp implements IItem, IStatic { @Override public int getCurrentHealth() { return 0; } @Override - public int getDefence() { + public int getDefense() { return 0; } @@ -35,7 +39,8 @@ public class StairsUp implements IItem, IStatic { } @Override - public int handleDamage(IGame game, IItem source, int amount) { + public int handleDamage(Game game, Item source, int amount) { return 0; } + } diff --git a/src/main/java/inf101/v18/rogue101/objects/FakeWall.java b/src/main/java/inf101/v18/rogue101/object/Wall.java similarity index 66% rename from src/main/java/inf101/v18/rogue101/objects/FakeWall.java rename to src/main/java/inf101/v18/rogue101/object/Wall.java index 531b34e..010d15d 100644 --- a/src/main/java/inf101/v18/rogue101/objects/FakeWall.java +++ b/src/main/java/inf101/v18/rogue101/object/Wall.java @@ -1,10 +1,14 @@ -package inf101.v18.rogue101.objects; +package inf101.v18.rogue101.object; import inf101.v18.gfx.gfxmode.ITurtle; import inf101.v18.gfx.textmode.BlocksAndBoxes; -import inf101.v18.rogue101.game.IGame; +import inf101.v18.rogue101.game.Game; + +/** + * A solid wall blocking passage + */ +public class Wall implements Item { -public class FakeWall implements IItem { private int hp = getMaxHealth(); @Override @@ -18,18 +22,18 @@ public class FakeWall implements IItem { } @Override - public int getDefence() { + public int getDefense() { return 10; } @Override public int getMaxHealth() { - return 1; + return 1000; } @Override public String getName() { - return "fake wall"; + return "wall"; } @Override @@ -39,12 +43,13 @@ public class FakeWall implements IItem { @Override public String getSymbol() { - return "\u001b[47m" + BlocksAndBoxes.BLOCK_FULL + "\u001b[0m"; + return BlocksAndBoxes.BLOCK_FULL; } @Override - public int handleDamage(IGame game, IItem source, int amount) { + public int handleDamage(Game game, Item source, int amount) { hp -= amount; return amount; } + } diff --git a/src/main/java/inf101/v18/rogue101/objects/Dust.java b/src/main/java/inf101/v18/rogue101/objects/Dust.java deleted file mode 100644 index 2491b6d..0000000 --- a/src/main/java/inf101/v18/rogue101/objects/Dust.java +++ /dev/null @@ -1,49 +0,0 @@ -package inf101.v18.rogue101.objects; - -import inf101.v18.gfx.gfxmode.ITurtle; -import inf101.v18.gfx.textmode.BlocksAndBoxes; -import inf101.v18.rogue101.game.IGame; - -public class Dust implements IItem { - - @Override - public boolean draw(ITurtle painter, double w, double h) { - return false; - } - - @Override - public int getCurrentHealth() { - return 0; - } - - @Override - public int getDefence() { - return 0; - } - - @Override - public int getMaxHealth() { - return 0; - } - - @Override - public String getName() { - return "thick layer of dust"; - } - - @Override - public int getSize() { - return 1; - } - - @Override - public String getSymbol() { - return BlocksAndBoxes.BLOCK_HALF; - } - - @Override - public int handleDamage(IGame game, IItem source, int amount) { - return 0; - } - -} diff --git a/src/main/java/inf101/v18/rogue101/objects/IActor.java b/src/main/java/inf101/v18/rogue101/objects/IActor.java deleted file mode 100644 index a8062c5..0000000 --- a/src/main/java/inf101/v18/rogue101/objects/IActor.java +++ /dev/null @@ -1,40 +0,0 @@ -package inf101.v18.rogue101.objects; - -/** - * An actor is an IItem that can also do something, either controlled by the - * computer (INonPlayer) or the user (IPlayer). - * - * @author anya - * - */ -public interface IActor extends IItem { - /** - * @return This actor's attack score (used against an item's - * {@link #getDefence()} score to see if an attack is successful) - */ - int getAttack(); - - /** - * @return The damage this actor deals on a successful attack (used together - * with - * {@link #handleDamage(inf101.v18.rogue101.game.IGame, IItem, int)} on - * the target) - */ - int getDamage(); - - /** - * How many tiles the IActor is able to see in each direction. - * - * @return Number of tiles - */ - default int getVision() { - return 1; - } - - /** - * Gets an item of the specified type, if the actor has the item. - * - * @return An IItem or null - */ - IItem getItem(Class type); -} diff --git a/src/main/java/inf101/v18/rogue101/objects/IItem.java b/src/main/java/inf101/v18/rogue101/objects/IItem.java deleted file mode 100644 index 62bf742..0000000 --- a/src/main/java/inf101/v18/rogue101/objects/IItem.java +++ /dev/null @@ -1,181 +0,0 @@ -package inf101.v18.rogue101.objects; - -import inf101.v18.gfx.gfxmode.ITurtle; -import inf101.v18.rogue101.events.IEvent; -import inf101.v18.rogue101.game.IGame; - -/** - * An {@link IItem} is something that can be placed on the map or into a - * container of items. - *

    - * An {@link IActor} is a special case of {@link IItem} used for things that are - * “alive”, such as the player or AI-controlled monsters/NPCs. - *

    - * By default, all items have a hit point / health system (they can be damaged), - * and can be picked up. So you could potentially destroy an item or pick up a - * monster. - * - * @author anya - */ -public interface IItem extends Comparable { - @Override - default int compareTo(IItem other) { - return Integer.compare(getSize(), other.getSize()); - } - - /** - * Draw this item on the screen. - *

    - * The turtle-painter will be positioned in the centre of the cell. You should - * avoid drawing outside the cell (size is indicated by the w and - * h parameters, so you can move ±w/2 and ±h/2 from the initial - * position). If you want to start in the lower left corner of the cell, you can - * start by doing painter.jump(-w/2,h/2) ((0,0) is in the top-left - * corner of the screen, so negative X points left and positive Y points down). - *

    - * If this method returns true, the game will not print - * {@link #getSymbol()} in the cell. - *

    - * All calls to painter.save() must be matched by a call to - * painter.restore(). - * - * @param painter - * A turtle-painter for drawing - * @param w - * The width of the cell - * @param h - * The height of the cell - * @return False if the letter from {@link #getSymbol()} should be drawn instead - */ - default boolean draw(ITurtle painter, double w, double h) { - return false; - } - - /** - * @return "a" or "an", depending on the name - */ - default String getArticle() { - return "a"; - } - - /** - * Get current remaining health points. - *

    - * An object's health points determines how much damage it can take - * before it is destroyed / broken / killed. - * - * @return Current health points for this item - */ - int getCurrentHealth(); - - /** - * The defence score determines how hard an object/actor is to hit or grab. - * - * @return Defence score of this object - */ - int getDefence(); - - /** - * Get item health as a 0.0..1.0 proportion. - * - *

  • getHealth() >= 1.0 means perfect condition - *
  • getHealth() <= 0.0 means broken or dead - *
  • 0.0 < getHealth() < 1.0 means partially damaged - * - * @return Health, in the range 0.0 to 1.0 - */ - default double getHealthStatus() { - return getMaxHealth() > 0 ? getCurrentHealth() / getMaxHealth() : 0; - } - - /** - * Get maximum health points. - * - * An object's health points determines how much damage it can take - * before it is destroyed / broken / killed. - * - * @return Max health points for this item - */ - int getMaxHealth(); - - /** - * Get a (user-friendly) name for the item - *

    - * Used for things like "You see " + getArticle() + " " + getName() - * - * @return Item's name - */ - String getName(); - - /** - * Get a map symbol used for printing this item on the screen. - *

    - * This is usually the same as {@link #getSymbol()}, but could also include - * special control characters for changing the text colour, for example. - * - * - * @return A string to be displayed for this item on the screen (should be only - * one column wide when printed) - * @see ANSI - * escape code (on Wikipedia) - */ - default String getPrintSymbol() { - return getSymbol(); - } - - /** - * Get the size of the object. - *

    - * The size determines how much space an item will use if put into a container. - * - * @return Size of the item - */ - int getSize(); - - /** - * Get the map symbol of this item. - *

    - * The symbol can be used on a text-only map, or when loading a map from text. - *

    - * The symbol should be a single Unicode codepoint (i.e., - * getSymbol().codePointCount(0, getSymbol().length()) == 1). In - * most cases this means that the symbol should be a single character (i.e., - * getSymbol().length() == 1); but there are a few Unicode characters (such as - * many emojis and special symbols) that require two Java chars. - * - * @return A single-codepoint string with the item's symbol - */ - String getSymbol(); - - /** - * Inform the item that it has been damaged - * - * @param game - * The game - * @param source - * The item (usually an IActor) that caused the damage - * @param amount - * How much damage the item should take - * @return Amount of damage actually taken (could be less than - * amount due to armour/protection effects) - */ - int handleDamage(IGame game, IItem source, int amount); - - /** - * Inform the item that something has happened. - * - * @param event - * An object describing the event. - * @return - */ - default T handleEvent(IEvent event) { - return event.getData(); - } - - /** - * @return True if this item has been destroyed, and should be removed from the map - */ - default boolean isDestroyed() { - return getCurrentHealth() < 0; - } -} diff --git a/src/main/java/inf101/v18/rogue101/objects/INonPlayer.java b/src/main/java/inf101/v18/rogue101/objects/INonPlayer.java deleted file mode 100644 index cbae489..0000000 --- a/src/main/java/inf101/v18/rogue101/objects/INonPlayer.java +++ /dev/null @@ -1,22 +0,0 @@ -package inf101.v18.rogue101.objects; - -import inf101.v18.rogue101.game.IGame; - -/** - * An actor controlled by the computer - * - * @author anya - * - */ -public interface INonPlayer extends IActor { - /** - * Do one turn for this non-player - *

    - * This INonPlayer will be the game's current actor ({@link IGame#getActor()}) - * for the duration of this method call. - * - * @param game - * Game, for interacting with the world - */ - void doTurn(IGame game); -} diff --git a/src/main/java/inf101/v18/rogue101/objects/IPlayer.java b/src/main/java/inf101/v18/rogue101/objects/IPlayer.java deleted file mode 100644 index 5a6281a..0000000 --- a/src/main/java/inf101/v18/rogue101/objects/IPlayer.java +++ /dev/null @@ -1,26 +0,0 @@ -package inf101.v18.rogue101.objects; - -import inf101.v18.rogue101.game.IGame; -import javafx.scene.input.KeyCode; - -public interface IPlayer extends IActor { - /** - * Send key presses from the human player to the player object. - *

    - * The player object should interpret the key presses, and then perform its - * moves or whatever, according to the game's rules and the player's - * instructions. - *

    - * This IPlayer will be the game's current actor ({@link IGame#getActor()}) and - * be at {@link IGame#getLocation()}, when this method is called. - *

    - * This method may be called many times in a single turn; the turn ends - * {@link #keyPressed(IGame, KeyCode)} returns and the player has used its - * movement points (e.g., by calling {@link IGame#move(inf101.v18.grid.GridDirection)}). - * - * @param game - * Game, for interacting with the world - * @return True if the player has done anything consuming a turn. False otherwise - */ - boolean keyPressed(IGame game, KeyCode key); -} diff --git a/src/main/java/inf101/v18/rogue101/objects/Wall.java b/src/main/java/inf101/v18/rogue101/objects/Wall.java deleted file mode 100644 index 32bb23b..0000000 --- a/src/main/java/inf101/v18/rogue101/objects/Wall.java +++ /dev/null @@ -1,50 +0,0 @@ -package inf101.v18.rogue101.objects; - -import inf101.v18.gfx.gfxmode.ITurtle; -import inf101.v18.gfx.textmode.BlocksAndBoxes; -import inf101.v18.rogue101.game.IGame; - -public class Wall implements IItem { - private int hp = getMaxHealth(); - - @Override - public boolean draw(ITurtle painter, double w, double h) { - return false; - } - - @Override - public int getCurrentHealth() { - return hp; - } - - @Override - public int getDefence() { - return 10; - } - - @Override - public int getMaxHealth() { - return 1000; - } - - @Override - public String getName() { - return "wall"; - } - - @Override - public int getSize() { - return Integer.MAX_VALUE; - } - - @Override - public String getSymbol() { - return BlocksAndBoxes.BLOCK_FULL; - } - - @Override - public int handleDamage(IGame game, IItem source, int amount) { - hp -= amount; - return amount; - } -} diff --git a/src/main/java/inf101/v18/rogue101/shared/NPC.java b/src/main/java/inf101/v18/rogue101/shared/NPC.java deleted file mode 100644 index dbd9e5c..0000000 --- a/src/main/java/inf101/v18/rogue101/shared/NPC.java +++ /dev/null @@ -1,226 +0,0 @@ -package inf101.v18.rogue101.shared; - -import inf101.v18.gfx.textmode.BlocksAndBoxes; -import inf101.v18.grid.GridDirection; -import inf101.v18.grid.ILocation; -import inf101.v18.rogue101.game.IGame; -import inf101.v18.rogue101.items.*; -import inf101.v18.rogue101.map.IMapView; -import inf101.v18.rogue101.objects.IActor; -import inf101.v18.rogue101.objects.IItem; -import inf101.v18.rogue101.objects.IPlayer; -import inf101.v18.rogue101.states.Attack; -import javafx.scene.media.AudioClip; -import javafx.scene.media.Media; -import javafx.scene.media.MediaPlayer; - -import java.io.File; -import java.io.FileNotFoundException; -import java.net.URL; -import java.util.List; - -/** - * A holder class for methods used by IActors. - */ -public class NPC { - - private NPC() {} - - /** - * Performs a ranged attack on the closest actor (if any) observable by the current actor if range is > 1. - * Performs a melee attack on an actor (if any) on a neighbouring tile if range == 1. - * Moves closer to an enemy if an attack is not possible. - * - * @param game An IGame object - * @param range The range of the equipped weapon - * @return True if an attack or a move towards an enemy was successful. False otherwise - */ - public static boolean tryAttack(IGame game, int range, Attack type) { - List neighbours = game.getVisible(); - for (ILocation neighbour : neighbours) { - IMapView map = game.getMap(); - if (map.hasActors(neighbour) && map.getActors(neighbour).get(0) instanceof IPlayer) { - ILocation current = game.getLocation(); - List dirs = game.locationDirection(current, neighbour); - IActor actor = game.getMap().getActors(neighbour).get(0); //We assume there is only one actor. - for (GridDirection dir : dirs) { - if (type == Attack.MELEE - && current.gridDistanceTo(neighbour) <= 1 - && current.canGo(dir) - && game.getMap().has(current.go(dir), actor) - ) { - game.attack(dir, actor); - return true; - } - if (range > 1 && current.gridDistanceTo(neighbour) <= range) { - game.rangedAttack(dir, actor, type); - return true; - } - } - for (GridDirection dir : dirs) { - if (game.canGo(dir)) { - game.move(dir); - return true; - } - } - } - } - return false; - } - - /** - * Tries to find and reach an item of a specified class. - * - * @param game An IGame object - * @param validItems A list of classes the NPC should look for - * @return True if an appropriate item was found in the range of the current actor. False otherwise. - */ - public static boolean trackItem(IGame game, List> validItems) { - List neighbours = game.getVisible(); - for (ILocation neighbour : neighbours) { - for (IItem item : game.getMap().getAll(neighbour)) { - for (Class validItem : validItems) { - if (validItem.isInstance(item)) { - ILocation current = game.getLocation(); - List dirs = game.locationDirection(current, neighbour); - for (GridDirection dir : dirs) { - if (game.canGo(dir)) { - game.move(dir); - return true; - } - } - } - } - } - } - return false; - } - - /** - * Picks up an item of one of the specified class types. - * - * @param game An IGame object - * @param validItems A list of classes the NPC should accept - * @return An item if it was successfully picked up. Null otherwise - */ - public static IItem pickUp(IGame game, List> validItems) { - for (IItem item : game.getLocalItems()) { - for (Class validItem : validItems) { - if (validItem.isInstance(item)) { - return game.pickUp(item); - } - } - } - return null; - } - - /** - * Makes an IActor go to the opposite direction of the closest enemy. - * Will return false if there are no visible enemies. - * - * @param game An IGame object - * @return True if the IActor fled. False otherwise - */ - public static boolean flee(IGame game) { - List neighbours = game.getVisible(); - for (ILocation neighbour : neighbours) { - if (game.getMap().hasActors(neighbour)) { - ILocation current = game.getLocation(); - List dirs = reverseDir(game.locationDirection(current, neighbour)); - for (GridDirection dir : dirs) { - if (game.canGo(dir)) { - game.move(dir); - return true; - } - } - } - } - return false; - } - - /** - * Reverses a list of directions. - * - * @param dirs A list directions - * @return A list of the opposite directions - */ - private static List reverseDir(List dirs) { - for (int i = 0; i < dirs.size(); i++) { - GridDirection dir = dirs.get(i); - switch (dir) { - case SOUTH: - dirs.set(i, GridDirection.NORTH); - break; - case NORTH: - dirs.set(i, GridDirection.SOUTH); - break; - case WEST: - dirs.set(i, GridDirection.EAST); - break; - case EAST: - dirs.set(i, GridDirection.WEST); - } - } - return dirs; - } - - /** - * Plays a sound. - * - * @param filename The String path of the audio file to play - */ - public static void playSound(String filename) { - URL url = NPC.class.getClassLoader().getResource(filename); - if (url != null) { - AudioClip clip = new AudioClip(url.toString()); - clip.play(); - } else { - System.out.println("Could not play audio file " + filename); - } - } - - /** - * Generates a hp bar based on item's current hp - * - * @return HP bar as string - */ - public static String hpBar(IItem item) { - StringBuilder str = new StringBuilder(BlocksAndBoxes.BLOCK_HALF); - int hp = item.getCurrentHealth(); - int hpLen = (int)((float)hp/item.getMaxHealth() * 10.0); - if (hp > 0 && hp < 100) { - str.append("\u001b[91m" + BlocksAndBoxes.BLOCK_BOTTOM + "\u001b[0m"); - hpLen++; - } else { - for (int i = 0; i < hpLen; i++) { - str.append("\u001b[91m" + BlocksAndBoxes.BLOCK_HALF + "\u001b[0m"); - } - } - for (int i = 0; i < 10 - hpLen; i++) { - str.append(" "); - } - str.append(BlocksAndBoxes.BLOCK_HALF); - return str.toString(); - } - - /** - * Gets the weapon appropriate for the used attack. - * - * @param type The attack type - * @return An IWeapon or null - */ - public static IWeapon getWeapon(Attack type, IActor actor) { - IWeapon weapon = null; - switch (type) { - case MELEE: - weapon = (IWeapon)actor.getItem(IMeleeWeapon.class); - break; - case RANGED: - weapon = (IWeapon)actor.getItem(IRangedWeapon.class); - break; - case MAGIC: - weapon = (IWeapon)actor.getItem(IMagicWeapon.class); - } - return weapon; - } -} diff --git a/src/main/java/inf101/v18/rogue101/state/AttackType.java b/src/main/java/inf101/v18/rogue101/state/AttackType.java new file mode 100644 index 0000000..950906f --- /dev/null +++ b/src/main/java/inf101/v18/rogue101/state/AttackType.java @@ -0,0 +1,12 @@ +package inf101.v18.rogue101.state; + +/** + * The type of a given attack + */ +public enum AttackType { + + MELEE, + MAGIC, + RANGED + +} diff --git a/src/main/java/inf101/v18/rogue101/states/Age.java b/src/main/java/inf101/v18/rogue101/state/LifeStage.java similarity index 56% rename from src/main/java/inf101/v18/rogue101/states/Age.java rename to src/main/java/inf101/v18/rogue101/state/LifeStage.java index bd497a2..ed24ea8 100644 --- a/src/main/java/inf101/v18/rogue101/states/Age.java +++ b/src/main/java/inf101/v18/rogue101/state/LifeStage.java @@ -1,16 +1,18 @@ -package inf101.v18.rogue101.states; +package inf101.v18.rogue101.state; import java.util.Random; /** - * An enum to distinguish different enemies of the same type. + * The age of an enemy, which alters some stats */ -public enum Age { +public enum LifeStage { + TODDLER, CHILD, TEEN, ADULT, ELDER; private static final Random random = new Random(); - public static Age getRandom() { + public static LifeStage getRandom() { return values()[random.nextInt(values().length)]; } + } diff --git a/src/main/java/inf101/v18/rogue101/state/Occupation.java b/src/main/java/inf101/v18/rogue101/state/Occupation.java new file mode 100644 index 0000000..90bd5fd --- /dev/null +++ b/src/main/java/inf101/v18/rogue101/state/Occupation.java @@ -0,0 +1,31 @@ +package inf101.v18.rogue101.state; + +import java.util.Random; + +/** + * The occupation of an enemy, which decides the type of attack it uses + */ +public enum Occupation { + + /** + * A knight that always uses melee attacks + */ + KNIGHT, + + /** + * A mage that always uses magic attacks + */ + MAGE, + + /** + * An archer that always uses ranged attacks + */ + ARCHER; + + private static final Random random = new Random(); + + public static Occupation getRandom() { + return values()[random.nextInt(values().length)]; + } + +} diff --git a/src/main/java/inf101/v18/rogue101/state/Personality.java b/src/main/java/inf101/v18/rogue101/state/Personality.java new file mode 100644 index 0000000..6cc2ec1 --- /dev/null +++ b/src/main/java/inf101/v18/rogue101/state/Personality.java @@ -0,0 +1,31 @@ +package inf101.v18.rogue101.state; + +import java.util.Random; + +/** + * The personality of an enemy, which defines its combat behavior + */ +public enum Personality { + + /** + * An enemy that attacks on sight + */ + AGGRESSIVE, + + /** + * An enemy that attacks if attacked + */ + CALM, + + /** + * An enemy that flees from battle + */ + AFRAID; + + private static final Random random = new Random(); + + public static Personality getRandom() { + return values()[random.nextInt(values().length)]; + } + +} diff --git a/src/main/java/inf101/v18/rogue101/state/Sound.java b/src/main/java/inf101/v18/rogue101/state/Sound.java new file mode 100644 index 0000000..4831eaa --- /dev/null +++ b/src/main/java/inf101/v18/rogue101/state/Sound.java @@ -0,0 +1,63 @@ +package inf101.v18.rogue101.state; + +import inf101.v18.util.NPCHelper; +import javafx.scene.media.AudioClip; + +import java.net.URL; + +/** + * A sound that can be played + */ +public enum Sound { + + MELEE_NO_WEAPON("Realistic_Punch-Mark_DiAngelo-1609462330.wav"), + MELEE_SWORD("Sword Swing-SoundBible.com-639083727.wav"), + RANGED_NO_WEAPON("Snow Ball Throw And Splat-SoundBible.com-992042947.wav"), + RANGED_BOW("Bow_Fire_Arrow-Stephan_Schutze-2133929391.wav"), + RANGED_STAFF("Large Fireball-SoundBible.com-301502490.wav"), + GAME_OVER("Dying-SoundBible.com-1255481835.wav"), + WIN("1_person_cheering-Jett_Rifkin-1851518140.wav"), + NPC_DEATH("oh-no-113125.wav"), + ; + + private final String soundFile; + private AudioClip audioClip = null; + + /** + * Instantiates a new sound file + * + * @param soundFile

    The file containing this sound

    + */ + Sound(String soundFile) { + this.soundFile = soundFile; + } + + /** + * Gets the sound file for this sound + * + * @return

    The sound file for this sound

    + */ + public String getSoundFile() { + return "audio/" + soundFile; + } + + /** + * Plays this sound + */ + public void play() { + if (audioClip == null) { + URL url = NPCHelper.class.getClassLoader().getResource(getSoundFile()); + if (url != null) { + audioClip = new AudioClip(url.toString()); + } else { + System.out.println("Could not play audio file " + getSoundFile()); + } + } + + // Play the sound in a new thread if it could be loaded + if (audioClip != null) { + new Thread(() -> audioClip.play()).start(); + } + } + +} diff --git a/src/main/java/inf101/v18/rogue101/states/Attack.java b/src/main/java/inf101/v18/rogue101/states/Attack.java deleted file mode 100644 index 3f0b3fc..0000000 --- a/src/main/java/inf101/v18/rogue101/states/Attack.java +++ /dev/null @@ -1,5 +0,0 @@ -package inf101.v18.rogue101.states; - -public enum Attack { - MELEE, MAGIC, RANGED -} diff --git a/src/main/java/inf101/v18/rogue101/states/Occupation.java b/src/main/java/inf101/v18/rogue101/states/Occupation.java deleted file mode 100644 index 63c82f2..0000000 --- a/src/main/java/inf101/v18/rogue101/states/Occupation.java +++ /dev/null @@ -1,16 +0,0 @@ -package inf101.v18.rogue101.states; - -import java.util.Random; - -/** - * An enum to distinguish different enemies of the same type. - */ -public enum Occupation { - KNIGHT, MAGE, BOWSMAN; - - private static final Random random = new Random(); - - public static Occupation getRandom() { - return values()[random.nextInt(values().length)]; - } -} diff --git a/src/main/java/inf101/v18/rogue101/states/Personality.java b/src/main/java/inf101/v18/rogue101/states/Personality.java deleted file mode 100644 index e3dd63c..0000000 --- a/src/main/java/inf101/v18/rogue101/states/Personality.java +++ /dev/null @@ -1,16 +0,0 @@ -package inf101.v18.rogue101.states; - -import java.util.Random; - -/** - * An enum to distinguish different enemies of the same type. - */ -public enum Personality { - AGRESSIVE, CALM, AFRAID; - - private static final Random random = new Random(); - - public static Personality getRandom() { - return values()[random.nextInt(values().length)]; - } -} diff --git a/src/main/java/inf101/v18/util/Generator.java b/src/main/java/inf101/v18/util/Generator.java new file mode 100644 index 0000000..a240667 --- /dev/null +++ b/src/main/java/inf101/v18/util/Generator.java @@ -0,0 +1,27 @@ +package inf101.v18.util; + +import java.util.Random; + +/** + * An interface for generators of random data values (to be used in testing). + * + * @param The type of data generated. + */ +public interface Generator { + + /** + * Generate a random object. + * + * @return An object of type T + */ + T generate(); + + /** + * Generate a random object. + * + * @param r A random generator + * @return An object of type T + */ + T generate(Random r); + +} diff --git a/src/main/java/inf101/v18/util/IGenerator.java b/src/main/java/inf101/v18/util/IGenerator.java deleted file mode 100644 index 811faa0..0000000 --- a/src/main/java/inf101/v18/util/IGenerator.java +++ /dev/null @@ -1,53 +0,0 @@ -package inf101.v18.util; - -import java.util.List; -import java.util.Random; - -/** - * An interface for generators of random data values (to be used in testing). - * - * @param - * The type of data generated. - */ -public interface IGenerator { - - /** - * Generate a random object. - * - * @return An object of type T - */ - T generate(); - - /** - * Generate a random object. - * - * @param r - * A random generator - * @return An object of type T - */ - T generate(Random r); - - /** - * Generate a number of equal objects. - * - * @param n - * The number of objects to generate. - * @return A list of objects, with the property that for any two objects a,b in - * the collection a.equals(b). - * - */ - List generateEquals(int n); - - /** - * Generate a number of equal objects. - * - * @param r - * A random generator - * @param n - * The number of objects to generate. - * @return A list of objects, with the property that for any two objects a,b in - * the collection a.equals(b). - * - */ - List generateEquals(Random r, int n); -} diff --git a/src/main/java/inf101/v18/util/NPCHelper.java b/src/main/java/inf101/v18/util/NPCHelper.java new file mode 100644 index 0000000..c4699c2 --- /dev/null +++ b/src/main/java/inf101/v18/util/NPCHelper.java @@ -0,0 +1,245 @@ +package inf101.v18.util; + +import inf101.v18.gfx.textmode.BlocksAndBoxes; +import inf101.v18.grid.GridDirection; +import inf101.v18.grid.Location; +import inf101.v18.rogue101.game.Game; +import inf101.v18.rogue101.items.weapon.MagicWeapon; +import inf101.v18.rogue101.items.weapon.MeleeWeapon; +import inf101.v18.rogue101.items.weapon.RangedWeapon; +import inf101.v18.rogue101.items.weapon.Weapon; +import inf101.v18.rogue101.map.MapView; +import inf101.v18.rogue101.object.Actor; +import inf101.v18.rogue101.object.Item; +import inf101.v18.rogue101.object.PlayerCharacter; +import inf101.v18.rogue101.state.AttackType; + +import java.util.List; + +/** + * A holder class for methods used by IActors. + */ +public class NPCHelper { + + private NPCHelper() { + } + + /** + * Performs a ranged attack on the closest actor (if any) observable by the current actor if range is > 1. + * Performs a melee attack on an actor (if any) on a neighbouring tile if range == 1. + * Moves closer to an enemy if an attack is not possible. + * + * @param game An IGame object + * @param range The range of the equipped weapon + * @return True if an attack or a move towards an enemy was successful. False otherwise + */ + public static boolean tryAttack(Game game, int range, AttackType type) { + List neighbours = game.getVisible(); + for (Location neighbour : neighbours) { + MapView map = game.getMap(); + if (map.hasActors(neighbour) && map.getActors(neighbour).get(0) instanceof PlayerCharacter) { + Location current = game.getLocation(); + List dirs = game.locationDirection(current, neighbour); + Actor actor = game.getMap().getActors(neighbour).get(0); //We assume there is only one actor. + for (GridDirection dir : dirs) { + if (type == AttackType.MELEE + && current.gridDistanceTo(neighbour) <= 1 + && current.canGo(dir) + && game.getMap().has(current.go(dir), actor) + ) { + game.attack(dir, actor); + return true; + } + if (range > 1 && current.gridDistanceTo(neighbour) <= range) { + game.rangedAttack(dir, actor, type); + return true; + } + } + for (GridDirection dir : dirs) { + if (game.canGo(dir)) { + game.move(dir); + return true; + } + } + } + } + return false; + } + + /** + * Tries to find and reach an item of a specified class. + * + * @param game An IGame object + * @param validItems A list of classes the NPC should look for + * @return True if an appropriate item was found in the range of the current actor. False otherwise. + */ + public static boolean trackItem(Game game, List> validItems) { + List neighbours = game.getVisible(); + for (Location neighbour : neighbours) { + for (Item item : game.getMap().getAll(neighbour)) { + if (!isValid(item, validItems)) { + continue; + } + + // If an item is found, go towards the item + Location current = game.getLocation(); + List directions = game.locationDirection(current, neighbour); + for (GridDirection direction : directions) { + if (game.canGo(direction)) { + game.move(direction); + return true; + } + } + } + } + return false; + } + + /** + * Checks whether an item's class is in the list of valid item classes + * + * @param item

    The item to check

    + * @param validItems

    Valid item classes to look for

    + * @return

    True if the item is valid

    + */ + private static boolean isValid(Item item, List> validItems) { + for (Class validItem : validItems) { + if (validItem.isInstance(item)) { + return true; + } + } + return false; + } + + /** + * Picks up an item of one of the specified class types. + * + * @param game An IGame object + * @param validItems A list of classes the NPC should accept + * @return An item if it was successfully picked up. Null otherwise + */ + public static Item pickUp(Game game, List> validItems) { + for (Item item : game.getLocalItems()) { + for (Class validItem : validItems) { + if (validItem.isInstance(item)) { + return game.pickUp(item); + } + } + } + return null; + } + + /** + * Makes an Actor go to the opposite direction of the closest enemy. + * + *

    Will return false if there are no visible enemies.

    + * + * @param game

    A Game object

    + * @return

    True if the Actor fled. False otherwise

    + */ + public static boolean flee(Game game) { + List neighbours = game.getVisible(); + for (Location neighbour : neighbours) { + if (!game.getMap().hasActors(neighbour)) { + continue; + } + + Location current = game.getLocation(); + List directions = reverseDirections(game.locationDirection(current, neighbour)); + for (GridDirection direction : directions) { + if (game.canGo(direction)) { + game.move(direction); + return true; + } + } + } + return false; + } + + /** + * Gets the reverse of a given direction + * + * @param direction

    The direction to get the reverse of

    + * @return

    The reverse direction

    + */ + public static GridDirection getReverseDirection(GridDirection direction) { + switch (direction) { + case SOUTH: + return GridDirection.NORTH; + case NORTH: + return GridDirection.SOUTH; + case WEST: + return GridDirection.EAST; + case EAST: + return GridDirection.WEST; + case NORTHEAST: + return GridDirection.SOUTHWEST; + case NORTHWEST: + return GridDirection.SOUTHEAST; + case SOUTHEAST: + return GridDirection.NORTHWEST; + case SOUTHWEST: + return GridDirection.NORTHEAST; + case CENTER: + return GridDirection.CENTER; + default: + throw new IllegalArgumentException("Unknown direction encountered"); + } + } + + /** + * Reverses a list of directions. + * + * @param directions A list directions + * @return A list of the opposite directions + */ + private static List reverseDirections(List directions) { + directions.replaceAll(NPCHelper::getReverseDirection); + return directions; + } + + /** + * Generates a hp bar based on item's current hp + * + * @return HP bar as string + */ + public static String hpBar(Item item) { + StringBuilder str = new StringBuilder(BlocksAndBoxes.BLOCK_HALF); + int hp = item.getCurrentHealth(); + int hpLen = (int) ((float) hp / item.getMaxHealth() * 10.0); + if (hp > 0 && hp < 100) { + str.append("\u001b[91m" + BlocksAndBoxes.BLOCK_BOTTOM + "\u001b[0m"); + hpLen++; + } else { + for (int i = 0; i < hpLen; i++) { + str.append("\u001b[91m" + BlocksAndBoxes.BLOCK_HALF + "\u001b[0m"); + } + } + for (int i = 0; i < 10 - hpLen; i++) { + str.append(" "); + } + str.append(BlocksAndBoxes.BLOCK_HALF); + return str.toString(); + } + + /** + * Gets the weapon appropriate for the used attack. + * + * @param type The attack type + * @return An IWeapon or null + */ + public static Weapon getWeapon(AttackType type, Actor actor) { + Weapon weapon = null; + switch (type) { + case MELEE: + weapon = (Weapon) actor.getItem(MeleeWeapon.class); + break; + case RANGED: + weapon = (Weapon) actor.getItem(RangedWeapon.class); + break; + case MAGIC: + weapon = (Weapon) actor.getItem(MagicWeapon.class); + } + return weapon; + } +} diff --git a/src/main/java/inf101/v18/util/generators/AbstractGenerator.java b/src/main/java/inf101/v18/util/generators/AbstractGenerator.java index d64405e..e3c88ef 100644 --- a/src/main/java/inf101/v18/util/generators/AbstractGenerator.java +++ b/src/main/java/inf101/v18/util/generators/AbstractGenerator.java @@ -1,35 +1,15 @@ package inf101.v18.util.generators; -import java.util.ArrayList; -import java.util.List; +import inf101.v18.util.Generator; + import java.util.Random; -import inf101.v18.util.IGenerator; +public abstract class AbstractGenerator implements Generator { + private static final Random commonRandom = new Random(); -public abstract class AbstractGenerator implements IGenerator { - private static final Random commonRandom = new Random(); - - @Override - public T generate() { - return generate(commonRandom); - } - - @Override - public List generateEquals(int n) { - return generateEquals(commonRandom, n); - } - - @Override - public List generateEquals(Random r, int n) { - long seed = r.nextLong(); - - List list = new ArrayList(); - - for (int i = 0; i < n; i++) { - list.add(generate(new Random(seed))); - } - - return list; - } + @Override + public T generate() { + return generate(commonRandom); + } } diff --git a/src/main/java/inf101/v18/util/generators/AreaGenerator.java b/src/main/java/inf101/v18/util/generators/AreaGenerator.java index 4eac01a..a43e36e 100644 --- a/src/main/java/inf101/v18/util/generators/AreaGenerator.java +++ b/src/main/java/inf101/v18/util/generators/AreaGenerator.java @@ -2,45 +2,45 @@ package inf101.v18.util.generators; import inf101.v18.grid.IArea; import inf101.v18.grid.RectArea; -import inf101.v18.util.IGenerator; +import inf101.v18.util.Generator; import java.util.Random; public class AreaGenerator extends AbstractGenerator { - /** - * Generator for the x-coordinate - */ - private final IGenerator xGenerator; - /** - * Generator for the y-coordinate - */ - private final IGenerator yGenerator; + /** + * Generator for the x-coordinate + */ + private final Generator xGenerator; + /** + * Generator for the y-coordinate + */ + private final Generator yGenerator; - /** - * Generate random Areas between (1,1) and (100,100) - */ - public AreaGenerator() { - this.xGenerator = new IntGenerator(1, 100); - this.yGenerator = new IntGenerator(1, 100); - } + /** + * Generate random Areas between (1,1) and (100,100) + */ + public AreaGenerator() { + this.xGenerator = new IntGenerator(1, 100); + this.yGenerator = new IntGenerator(1, 100); + } - /** - * Generate random Areas between (0,0) and (maxWidth,maxHeight) - * - * @param maxWidth - * @param maxHeight - */ - public AreaGenerator(int maxWidth, int maxHeight) { - if (maxWidth < 1 || maxHeight < 1) { - throw new IllegalArgumentException("Width and height must be 0 or greater"); - } + /** + * Generate random Areas between (0,0) and (maxWidth,maxHeight) + * + * @param maxWidth + * @param maxHeight + */ + public AreaGenerator(int maxWidth, int maxHeight) { + if (maxWidth < 1 || maxHeight < 1) { + throw new IllegalArgumentException("Width and height must be 0 or greater"); + } - this.xGenerator = new IntGenerator(1, maxWidth); - this.yGenerator = new IntGenerator(1, maxHeight); - } + this.xGenerator = new IntGenerator(1, maxWidth); + this.yGenerator = new IntGenerator(1, maxHeight); + } - @Override - public IArea generate(Random r) { - return new RectArea(xGenerator.generate(r), yGenerator.generate(r)); - } + @Override + public IArea generate(Random r) { + return new RectArea(xGenerator.generate(r), yGenerator.generate(r)); + } } diff --git a/src/main/java/inf101/v18/util/generators/DoubleGenerator.java b/src/main/java/inf101/v18/util/generators/DoubleGenerator.java index 1848cbb..e5848b4 100644 --- a/src/main/java/inf101/v18/util/generators/DoubleGenerator.java +++ b/src/main/java/inf101/v18/util/generators/DoubleGenerator.java @@ -4,84 +4,80 @@ import java.util.Random; /** * Generator for doubles, with uniform distribution. - * + *

    * Will never generate positive or negative infinity or NaN. * * @author anya - * */ public class DoubleGenerator extends AbstractGenerator { - private final double minValue; + private final double minValue; - private final double diff; + private final double diff; - /** - * Make a generator for doubles between 0.0 (inclusive) and 1.0 (exclusive). - */ - public DoubleGenerator() { - this.minValue = 0; - diff = 1.0; - } + /** + * Make a generator for doubles between 0.0 (inclusive) and 1.0 (exclusive). + */ + public DoubleGenerator() { + this.minValue = 0; + diff = 1.0; + } - /** - * Make a generator for positive doubles from 0 (inclusive) to maxValue - * (exclusive). - * - * @param maxValue - * The max value, or 0 for the full range of positive doubles - */ - public DoubleGenerator(double maxValue) { - if (maxValue < 0.0) { - throw new IllegalArgumentException("maxValue must be positive or 0.0"); - } - this.minValue = 0.0; - double mv = Double.MAX_VALUE; + /** + * Make a generator for positive doubles from 0 (inclusive) to maxValue + * (exclusive). + * + * @param maxValue The max value, or 0 for the full range of positive doubles + */ + public DoubleGenerator(double maxValue) { + if (maxValue < 0.0) { + throw new IllegalArgumentException("maxValue must be positive or 0.0"); + } + this.minValue = 0.0; + double mv = Double.MAX_VALUE; - if (maxValue != 0.0) { - mv = maxValue; - } - diff = mv - minValue; - } + if (maxValue != 0.0) { + mv = maxValue; + } + diff = mv - minValue; + } - /** - * Make a generator for numbers from minValue (inclusive) to maxValue - * (exclusive). - * - * Due to the way the numbers are constructed, the range from minValue to - * maxValue can only span half the total range of available double values. If - * the range is too large the bounds will be divided by two (you can check - * whether they range is too large by checking - * Double.isInfinite(maxValue-minValue)). - * - * @param minValue - * The minimum value - * @param maxValue - * The maximum value, minValue < maxValue - */ - public DoubleGenerator(double minValue, double maxValue) { - if (minValue >= maxValue) { - throw new IllegalArgumentException("minValue must be less than maxValue"); - } - if (Double.isInfinite(minValue)) { - minValue = -Double.MAX_VALUE / 2.0; - } - if (Double.isInfinite(maxValue)) { - maxValue = Double.MAX_VALUE / 2.0; - } - if (Double.isInfinite(maxValue - minValue)) { - maxValue /= 2.0; - minValue /= 2.0; - } - this.minValue = minValue; - diff = maxValue - minValue; + /** + * Make a generator for numbers from minValue (inclusive) to maxValue + * (exclusive). + *

    + * Due to the way the numbers are constructed, the range from minValue to + * maxValue can only span half the total range of available double values. If + * the range is too large the bounds will be divided by two (you can check + * whether they range is too large by checking + * Double.isInfinite(maxValue-minValue)). + * + * @param minValue The minimum value + * @param maxValue The maximum value, minValue < maxValue + */ + public DoubleGenerator(double minValue, double maxValue) { + if (minValue >= maxValue) { + throw new IllegalArgumentException("minValue must be less than maxValue"); + } + if (Double.isInfinite(minValue)) { + minValue = -Double.MAX_VALUE / 2.0; + } + if (Double.isInfinite(maxValue)) { + maxValue = Double.MAX_VALUE / 2.0; + } + if (Double.isInfinite(maxValue - minValue)) { + maxValue /= 2.0; + minValue /= 2.0; + } + this.minValue = minValue; + diff = maxValue - minValue; - assert !Double.isInfinite(diff); - } + assert !Double.isInfinite(diff); + } - @Override - public Double generate(Random rng) { - double d = rng.nextDouble(); + @Override + public Double generate(Random rng) { + double d = rng.nextDouble(); - return minValue + (d * diff); - } + return minValue + (d * diff); + } } diff --git a/src/main/java/inf101/v18/util/generators/ElementGenerator.java b/src/main/java/inf101/v18/util/generators/ElementGenerator.java index 1d44e76..dc6eae0 100644 --- a/src/main/java/inf101/v18/util/generators/ElementGenerator.java +++ b/src/main/java/inf101/v18/util/generators/ElementGenerator.java @@ -6,33 +6,35 @@ import java.util.List; import java.util.Random; public class ElementGenerator extends AbstractGenerator { - private List elts; + private List elts; - /** - * New ElementGenerator, will pick a random element from a collection. - * - * @requires collection must not be empty - */ - public ElementGenerator(Collection elts) { - if (elts.size() == 0) - throw new IllegalArgumentException(); - this.elts = new ArrayList<>(elts); - } + /** + * New ElementGenerator, will pick a random element from a collection. + * + * @requires collection must not be empty + */ + public ElementGenerator(Collection elts) { + if (elts.size() == 0) { + throw new IllegalArgumentException(); + } + this.elts = new ArrayList<>(elts); + } - /** - * New ElementGenerator, will pick a random element from a list. - * - * @requires list must not be empty - */ - public ElementGenerator(List elts) { - if (elts.size() == 0) - throw new IllegalArgumentException(); - this.elts = elts; - } + /** + * New ElementGenerator, will pick a random element from a list. + * + * @requires list must not be empty + */ + public ElementGenerator(List elts) { + if (elts.size() == 0) { + throw new IllegalArgumentException(); + } + this.elts = elts; + } - @Override - public T generate(Random r) { - return elts.get(r.nextInt(elts.size())); - } + @Override + public T generate(Random r) { + return elts.get(r.nextInt(elts.size())); + } } diff --git a/src/main/java/inf101/v18/util/generators/GridDirectionGenerator.java b/src/main/java/inf101/v18/util/generators/GridDirectionGenerator.java index 7d4b377..fa644e8 100644 --- a/src/main/java/inf101/v18/util/generators/GridDirectionGenerator.java +++ b/src/main/java/inf101/v18/util/generators/GridDirectionGenerator.java @@ -1,21 +1,21 @@ package inf101.v18.util.generators; -import java.util.List; - import inf101.v18.grid.GridDirection; -public class GridDirectionGenerator extends ElementGenerator { - /** - * New DirectionGenerator, will generate directions between 0° and 360° - */ - public GridDirectionGenerator() { - super(GridDirection.EIGHT_DIRECTIONS); - } +import java.util.List; - /** - * New DirectionGenerator, will generate directions between minValue and maxVaue - */ - public GridDirectionGenerator(List dirs) { - super(dirs); - } +public class GridDirectionGenerator extends ElementGenerator { + /** + * New DirectionGenerator, will generate directions between 0° and 360° + */ + public GridDirectionGenerator() { + super(GridDirection.EIGHT_DIRECTIONS); + } + + /** + * New DirectionGenerator, will generate directions between minValue and maxVaue + */ + public GridDirectionGenerator(List dirs) { + super(dirs); + } } diff --git a/src/main/java/inf101/v18/util/generators/GridGenerator.java b/src/main/java/inf101/v18/util/generators/GridGenerator.java index ea1485b..0840a5f 100644 --- a/src/main/java/inf101/v18/util/generators/GridGenerator.java +++ b/src/main/java/inf101/v18/util/generators/GridGenerator.java @@ -2,77 +2,72 @@ package inf101.v18.util.generators; import inf101.v18.grid.IGrid; import inf101.v18.grid.MyGrid; -import inf101.v18.util.IGenerator; +import inf101.v18.util.Generator; import java.util.Random; public class GridGenerator extends AbstractGenerator> { - /** - * Generator for the width of a random grid - */ - private final IGenerator widthGenerator; - /** - * Generator for the height of a random grid - */ - private final IGenerator heightGenerator; - /** - * Generator for one element of a random grid - */ - private final IGenerator elementGenerator; + /** + * Generator for the width of a random grid + */ + private final Generator widthGenerator; + /** + * Generator for the height of a random grid + */ + private final Generator heightGenerator; + /** + * Generator for one element of a random grid + */ + private final Generator elementGenerator; - /** - * Generator for random 2D grids. Each dimension will be from 1 to 100. - * - * @param elementGenerator - * Generator for the elements - */ - public GridGenerator(IGenerator elementGenerator) { - this.elementGenerator = elementGenerator; - this.widthGenerator = new IntGenerator(1, 100); - this.heightGenerator = new IntGenerator(1, 100); - } + /** + * Generator for random 2D grids. Each dimension will be from 1 to 100. + * + * @param elementGenerator Generator for the elements + */ + public GridGenerator(Generator elementGenerator) { + this.elementGenerator = elementGenerator; + this.widthGenerator = new IntGenerator(1, 100); + this.heightGenerator = new IntGenerator(1, 100); + } - /** - * Generator for random 2D grids using the supplied generators to generate width - * and height. - * - * @param elementGenerator - * Generator for the elements - * @param widthGenerator - * Should only generate positive numbers - * @param heightGenerator - * Should only generate positive numbers - */ - public GridGenerator(IGenerator elementGenerator, IGenerator widthGenerator, - IGenerator heightGenerator) { - this.elementGenerator = elementGenerator; - this.widthGenerator = widthGenerator; - this.heightGenerator = heightGenerator; - } + /** + * Generator for random 2D grids using the supplied generators to generate width + * and height. + * + * @param elementGenerator Generator for the elements + * @param widthGenerator Should only generate positive numbers + * @param heightGenerator Should only generate positive numbers + */ + public GridGenerator(Generator elementGenerator, Generator widthGenerator, + Generator heightGenerator) { + this.elementGenerator = elementGenerator; + this.widthGenerator = widthGenerator; + this.heightGenerator = heightGenerator; + } - /** - * Generator for random 2D grids with the given max dimensions. - * - * @param elementGenerator - * Generator for the elements - * @param maxWidth - * @param maxHeight - */ - public GridGenerator(IGenerator elementGenerator, int maxWidth, int maxHeight) { - if (maxWidth < 1 || maxHeight < 1) { - throw new IllegalArgumentException("Width and height must be 1 or greater"); - } + /** + * Generator for random 2D grids with the given max dimensions. + * + * @param elementGenerator Generator for the elements + * @param maxWidth + * @param maxHeight + */ + public GridGenerator(Generator elementGenerator, int maxWidth, int maxHeight) { + if (maxWidth < 1 || maxHeight < 1) { + throw new IllegalArgumentException("Width and height must be 1 or greater"); + } - this.elementGenerator = elementGenerator; - this.widthGenerator = new IntGenerator(1, maxWidth); - this.heightGenerator = new IntGenerator(1, maxHeight); - } + this.elementGenerator = elementGenerator; + this.widthGenerator = new IntGenerator(1, maxWidth); + this.heightGenerator = new IntGenerator(1, maxHeight); + } - @Override - public IGrid generate(Random r) { - int w = widthGenerator.generate(r); - int h = heightGenerator.generate(r); + @Override + public IGrid generate(Random r) { + int w = widthGenerator.generate(r); + int h = heightGenerator.generate(r); - return new MyGrid<>(w, h, (l) -> elementGenerator.generate(r)); - } + return new MyGrid<>(w, h, (l) -> elementGenerator.generate(r)); + } } diff --git a/src/main/java/inf101/v18/util/generators/IntGenerator.java b/src/main/java/inf101/v18/util/generators/IntGenerator.java index 8a6d619..8a38122 100644 --- a/src/main/java/inf101/v18/util/generators/IntGenerator.java +++ b/src/main/java/inf101/v18/util/generators/IntGenerator.java @@ -3,86 +3,80 @@ package inf101.v18.util.generators; import java.util.Random; public class IntGenerator extends AbstractGenerator { - /** - * Generate a random long in the interval [0,n) - * - * @param rng - * A random generator - * @param n - * The maximum value (exclusive) - * @return A uniformly distributed random integer in the range 0 (inclusive) to - * n (exclusive) - * - * @author Adapted from JDK implementation for nextInt(n) - */ - public static long nextLong(Random rng, long n) { - if (n <= 0) { - throw new IllegalArgumentException("n must be positive"); - } + /** + * Generate a random long in the interval [0,n) + * + * @param rng A random generator + * @param n The maximum value (exclusive) + * @return A uniformly distributed random integer in the range 0 (inclusive) to + * n (exclusive) + * @author Adapted from JDK implementation for nextInt(n) + */ + public static long nextLong(Random rng, long n) { + if (n <= 0) { + throw new IllegalArgumentException("n must be positive"); + } - long bits, val; - do { - bits = rng.nextLong() & ~Long.MIN_VALUE; // force to positive - val = bits % n; - } while (bits - val + (n - 1L) < 0L); - return val; - } + long bits, val; + do { + bits = rng.nextLong() & ~Long.MIN_VALUE; // force to positive + val = bits % n; + } while (bits - val + (n - 1L) < 0L); + return val; + } - private final long minValue; + private final long minValue; - private final long diff; + private final long diff; - /** - * Make a generator for the full range of integers. - */ - public IntGenerator() { - this.minValue = Integer.MIN_VALUE; - long maxValue = Integer.MAX_VALUE + 1L; // generate up to and including - // Integer.MAX_VALUE - diff = maxValue - minValue; - } + /** + * Make a generator for the full range of integers. + */ + public IntGenerator() { + this.minValue = Integer.MIN_VALUE; + long maxValue = Integer.MAX_VALUE + 1L; // generate up to and including + // Integer.MAX_VALUE + diff = maxValue - minValue; + } - /** - * Make a generator for positive numbers from 0 (inclusive) to maxValue - * (exclusive). - * - * @param maxValue - * The max value, or 0 for the full range of positive integers - */ - public IntGenerator(int maxValue) { - if (maxValue < 0) { - throw new IllegalArgumentException("maxValue must be positive or 0"); - } - this.minValue = 0; - long mv = Integer.MAX_VALUE + 1L; // generate up to and including - // Integer.MAX_VALUE - if (maxValue != 0) { - mv = maxValue; - } - diff = mv - minValue; - } + /** + * Make a generator for positive numbers from 0 (inclusive) to maxValue + * (exclusive). + * + * @param maxValue The max value, or 0 for the full range of positive integers + */ + public IntGenerator(int maxValue) { + if (maxValue < 0) { + throw new IllegalArgumentException("maxValue must be positive or 0"); + } + this.minValue = 0; + long mv = Integer.MAX_VALUE + 1L; // generate up to and including + // Integer.MAX_VALUE + if (maxValue != 0) { + mv = maxValue; + } + diff = mv - minValue; + } - /** - * Make a generator for numbers from minValue (inclusive) to maxValue - * (exclusive). - * - * @param minValue - * The minimum value - * @param maxValue - * The maximum value, minValue < maxValue - */ - public IntGenerator(int minValue, int maxValue) { - if (minValue >= maxValue) { - throw new IllegalArgumentException("minValue must be less than maxValue"); - } - this.minValue = minValue; - diff = ((long) maxValue) - ((long) minValue); - } + /** + * Make a generator for numbers from minValue (inclusive) to maxValue + * (exclusive). + * + * @param minValue The minimum value + * @param maxValue The maximum value, minValue < maxValue + */ + public IntGenerator(int minValue, int maxValue) { + if (minValue >= maxValue) { + throw new IllegalArgumentException("minValue must be less than maxValue"); + } + this.minValue = minValue; + diff = ((long) maxValue) - ((long) minValue); + } - @Override - public Integer generate(Random rng) { - long r = minValue + nextLong(rng, diff); + @Override + public Integer generate(Random rng) { + long r = minValue + nextLong(rng, diff); - return (int) r; - } + return (int) r; + } } diff --git a/src/main/java/inf101/v18/util/generators/ListGenerator.java b/src/main/java/inf101/v18/util/generators/ListGenerator.java index ab9a2c5..e3e0db8 100644 --- a/src/main/java/inf101/v18/util/generators/ListGenerator.java +++ b/src/main/java/inf101/v18/util/generators/ListGenerator.java @@ -1,44 +1,44 @@ package inf101.v18.util.generators; +import inf101.v18.util.Generator; + import java.util.ArrayList; import java.util.List; import java.util.Random; -import inf101.v18.util.IGenerator; - public class ListGenerator extends AbstractGenerator> { - /** - * Generator for the length of the list - */ - private final IGenerator lengthGenerator; + /** + * Generator for the length of the list + */ + private final Generator lengthGenerator; - /** - * Generator for one element of a random grid - */ - private final IGenerator elementGenerator; + /** + * Generator for one element of a random grid + */ + private final Generator elementGenerator; - public ListGenerator(IGenerator elementGenerator) { - this.elementGenerator = elementGenerator; - this.lengthGenerator = new IntGenerator(1, 100); - } + public ListGenerator(Generator elementGenerator) { + this.elementGenerator = elementGenerator; + this.lengthGenerator = new IntGenerator(1, 100); + } - public ListGenerator(IGenerator elementGenerator, int maxLength) { - if (maxLength < 1) { - throw new IllegalArgumentException("Length must be 1 or greater"); - } + public ListGenerator(Generator elementGenerator, int maxLength) { + if (maxLength < 1) { + throw new IllegalArgumentException("Length must be 1 or greater"); + } - this.elementGenerator = elementGenerator; - this.lengthGenerator = new IntGenerator(1, maxLength); - } + this.elementGenerator = elementGenerator; + this.lengthGenerator = new IntGenerator(1, maxLength); + } - @Override - public List generate(Random r) { - int l = lengthGenerator.generate(r); - List result = new ArrayList<>(l); + @Override + public List generate(Random r) { + int l = lengthGenerator.generate(r); + List result = new ArrayList<>(l); - for (int i = 0; i < l; i++) { - result.add(elementGenerator.generate(r)); - } - return result; - } + for (int i = 0; i < l; i++) { + result.add(elementGenerator.generate(r)); + } + return result; + } } diff --git a/src/main/java/inf101/v18/util/generators/LocationGenerator.java b/src/main/java/inf101/v18/util/generators/LocationGenerator.java index 342b204..b8df11c 100644 --- a/src/main/java/inf101/v18/util/generators/LocationGenerator.java +++ b/src/main/java/inf101/v18/util/generators/LocationGenerator.java @@ -1,23 +1,23 @@ package inf101.v18.util.generators; +import inf101.v18.grid.IArea; +import inf101.v18.grid.Location; + import java.util.Random; -import inf101.v18.grid.IArea; -import inf101.v18.grid.ILocation; +public class LocationGenerator extends AbstractGenerator { + private final IArea area; -public class LocationGenerator extends AbstractGenerator { - private final IArea area; + /** + * New LocationGenerator, will generate locations within the area + */ + public LocationGenerator(IArea area) { + this.area = area; + } - /** - * New LocationGenerator, will generate locations within the area - */ - public LocationGenerator(IArea area) { - this.area = area; - } - - @Override - public ILocation generate(Random r) { - return area.location(r.nextInt(area.getWidth()), r.nextInt(area.getHeight())); - } + @Override + public Location generate(Random r) { + return area.location(r.nextInt(area.getWidth()), r.nextInt(area.getHeight())); + } } diff --git a/src/main/java/inf101/v18/util/generators/StringGenerator.java b/src/main/java/inf101/v18/util/generators/StringGenerator.java index dc298dd..67742e4 100644 --- a/src/main/java/inf101/v18/util/generators/StringGenerator.java +++ b/src/main/java/inf101/v18/util/generators/StringGenerator.java @@ -4,56 +4,56 @@ import java.util.Random; public class StringGenerator extends AbstractGenerator { - private final int minLength; - private final int maxLength; + private final int minLength; + private final int maxLength; - public StringGenerator() { - this.minLength = 0; - this.maxLength = 15; - } + public StringGenerator() { + this.minLength = 0; + this.maxLength = 15; + } - public StringGenerator(int maxLength) { - if (maxLength < 0) { - throw new IllegalArgumentException("maxLength must be positive or 0"); - } - this.minLength = 0; - this.maxLength = maxLength; - } + public StringGenerator(int maxLength) { + if (maxLength < 0) { + throw new IllegalArgumentException("maxLength must be positive or 0"); + } + this.minLength = 0; + this.maxLength = maxLength; + } - public StringGenerator(int minLength, int maxLength) { - if (minLength >= maxLength) { - throw new IllegalArgumentException("minLength must be less than maxLength"); - } - this.minLength = minLength; - this.maxLength = maxLength; - } + public StringGenerator(int minLength, int maxLength) { + if (minLength >= maxLength) { + throw new IllegalArgumentException("minLength must be less than maxLength"); + } + this.minLength = minLength; + this.maxLength = maxLength; + } - @Override - public String generate(Random r) { - int len = r.nextInt(maxLength - minLength) + minLength; - StringBuilder b = new StringBuilder(); - for (int i = 0; i < len; i++) { - b.append(generateChar(r)); - } - return b.toString(); - } + @Override + public String generate(Random r) { + int len = r.nextInt(maxLength - minLength) + minLength; + StringBuilder b = new StringBuilder(); + for (int i = 0; i < len; i++) { + b.append(generateChar(r)); + } + return b.toString(); + } - public char generateChar(Random r) { - int kind = r.nextInt(7); + public char generateChar(Random r) { + int kind = r.nextInt(7); - switch (kind) { - case 0: - case 1: - case 2: - return (char) ('a' + r.nextInt(26)); - case 3: - case 4: - return (char) ('A' + r.nextInt(26)); - case 5: - return (char) ('0' + r.nextInt(10)); - case 6: - return (char) (' ' + r.nextInt(16)); - } - return ' '; - } + switch (kind) { + case 0: + case 1: + case 2: + return (char) ('a' + r.nextInt(26)); + case 3: + case 4: + return (char) ('A' + r.nextInt(26)); + case 5: + return (char) ('0' + r.nextInt(10)); + case 6: + return (char) (' ' + r.nextInt(16)); + } + return ' '; + } } diff --git a/src/main/resources/audio/oh-no-113125.wav b/src/main/resources/audio/oh-no-113125.wav new file mode 100644 index 0000000000000000000000000000000000000000..db3beb1295aa37b260be3b717b0afef752add848 GIT binary patch literal 167544 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

    hPe-OPxIU0 zw>xB4NOwec#9?4Fyoi1tJtcNp?D8HdMhQoU!K%}nKU9THhHn8k<$;*<$vf{FXP_d;krT>&IEK98FTQwJY-?JO% zG%jfSr_D>{sS-gJ{Sx~!`y7}f_M-Npz5%AkA>komk+@hq(`~xjeXsjoC6HzRFXWFO za6jO_PPk485-#>O&>r&!ItRnqb5hIXmWQ$jvWL}=s>5WVvd_(*n=Ozx1>@@0mEcFo?ab{Qs~M{SG_>iw z{et}z5|K6L#7QTZFo8w-+Mro?t) zr&6X-R&iEwd?h{-lc(8pneQ@RY#=u9N9fN`Yy>vq7Su)H1vUmZ1t$j#3~2Ld_j&H| z!b2lciB_^#v7;et0Z`Ovww+}+8cYWFZjbK1t-V_hH6Cu%$keibYo6C^uG|cc$)w7j zwY_Q~c&c$_>x$Ov9oIXMS?UMvd+jsJGm8tp*5lLg8z>toFBvZw*FfX+2Qt{TZfduS zo)fMitnCj!_ck6ItL<|_&n{W|(}Yyo`%NPb)& zxF5L@l5oix{#iZ=`X3+NKDjwUoFS-qM_fsd(jG5kUd4Qi{1&-BCN1WB{Ezr)(Nm-U z2YQ}G(nZqM!d1fU{9XJOW;1geZX0f{d7imeQ={n%g=B%9X+75}Yp88VX&lm+(ek@x zA#iGu9Kk2@$MQgVki5ISyB(!QtN+4#G#x4rva5wQo^1|HZRQc?5`MwDunwP!9}Ju& zIkA;^4@5`LozI<*Y>#X=?KkWjLBC3bBH75c@s{zHc5A!!z2m)Om~*JJ1O%R|ptiU+ zxmJR{W%YYfy!nBlDut`IJlESH2r&6doP2;h=F$SWBf+ofJx-M6{F z622Dh2mQ_l)_T@;)-6^6oK39Yr+EVXIE1~)w+gHyO(RUHnl#Ps&fT408*MuaKl7m0 z#PbnQ{YQG*TO#CRvx2{C(^4q^xKAnhOx>BAo4 z9^yi~OjskX7N3xu{BQQRT)F}ho&HEu#i`=a{89W#oXMR2jQ)&cgd>DM&P->ZHNaX8 zddz9v)4L-<6yLYLU;XR4S9Oc37gc8zWfZkQ6Kr($=V$@z(58{Dxu{I$!;T2S$ z_}=mH(LJJzLW)Apd7t-wD0(2e#JS8F#2m~lq7+ijpw6If>Tc?KDtjp(bw27m*m1Dq zY~z{6>owPFAQrr?54cXJH_m7j%S5ufuuo{}v~{Z+S2e0Yb1ecc!%oFE#lh}F-M&UY z;{?ZeM?Z9bbTBpu`yBrQuYlJp2Wk+02)+hpj7uONumD?teFGdXALtFOqpgK_<3yS# z)su>N$2P#+2I+e$TQ!Y}^QyW0_I)^cbF^e{f zHWdobBru;tMbRVZXUXTuO9@K}1MvOve{q>OgnM!Z@~ky>wH<50TK1UsnARKD8^iRW z`steKn%?R@kOX#6U9W0T{R>IpajICANF&lD!rk)>^l_0P?ilA7=QYp=jK__`rI3b^ z5I_1;+B4ct=1!)9-NE+Z`SJD%_6nMXO>hf2B>XP;A^4a7l%EUtV-Zu#`~q2?Hz~I$ zXuJz|$bQH^+Bnu2tB=-aXfiZJ1xazN@mM2#1NE;OUpG2yU9}7d;=Et|pEx z;{jv1CPLHD-O&A@{XzTq#tDsxx5KU4t-7ExzY^&gw8I5)b=m4NCk)MEA%`=dHmEkN zA*=z(61v~^uq_KRqL4E_#6_Z#@B{Ffq%6`Q$eNC)$I;V(yVXc;ChsBbCT*r{hV}F% z1L+SV>pb#(tl+QYpB9`J?1r@puuvR?FF%NyOq~OnX)`FZD4U6!h)T2){muE!$wVEh)zD{5Z1yl(M=+4zC(gWaXtQtcU-nLKSLXS_a6Z|XL6mnkX~VF0VvRqLunmBLDdp~bHjREL#^mj5XFUbM1sb>X(+ zEyb$x?(&`x>a)6eW%J&SeI4nVubP996=uX3F=y~+@ra+|9px@%uApr&I{KwwH#1&+_55F!Sl9@ zwis)awZu|l=?gu_rH;jpO|bVwp`uW}XkT;;HX1tyKN|m-_=NZ`+;_e3K6o#z7xn;l zKeh*@5a)D13=L=vCOCoiTffmQl4Bx+rx+$P_G z_gz8gk4wTKSxQ2z0NaJ@#?8jh!ruiJ{a*AQ^h4J}SAnC@vBbW_J{k1Gw~e=rof@TP zao3`*sLrU)FQ8p@wxZhLs#-r9pgyExN-^?Ytbkm{t8kX}srRX0*S5ATPg9`5S#j1a z=*{RVauvClS;VYhRDuh6DWg-^A?)wp-+y!X=J3@~tD|Pc%!*+HH|Ki%jrcz?zhmBp zy$f3i=Y`)8nA}s^Q~DHg#co6H2p8sFC9V<|+rToUDpQp~ZNY61n(j5dh0Oagm18Rl z%L~f8OS?+X6rC&jR`9JLy(qotWa+8Wm6a2bkv z!4URf_7IrmT_s*2Rzo&0L`_+1^|g9`O@L->_o(j6%BwI}2<@_UTe>Hyr>Fxp{+izU zKKfoTSNQ@p(lpXUH`B~t!6$@xZ&tyX!;9oeQqfd2|`g!dBb(Xbrg3Lmj^kTN9jlCZL~IUZO@}M z!TuJ;2xt5v|0365Yq6~^x$BVaplt(a(I$W%w6VRZeF)^9ysCOtC9AHlUQoZFUMJJZ zmenn-D`_li9N#gv1G!_zS>i3oT$McOHf zDGEvpwdE1?J?koKE9**SrSXLcg~xM_<#c8#vkv4O$k~~*0@|u9;O}NhK~jY4$`;W$lb`r2rz;w zQKcxtEyC?5RIQ{|f@ph2^G5UZOaqfiVNejph!gEZ&vDLi_O|!79|O(O59<%>Yx67f zOXDl!3d3^42GIA*An0^1{ChTw-BM+%w5@Tiaj~Eq6h;aCPwx+{bEXH!lQU5;QLtCC zM?!aFy2-?K;_-rUf*IUd+!w4Dta4f@4Vjl)P*xNMjB6T=TEkQER23bS9o7a*gRHE+ z49SD@f}Hb|jFb%1Z`1F~nU^!C=S|BaR*)-JH>_$Hq!_4(Fh!YOqF$gD6P6I3(w@-X zf`4qN$1sm}$l4I}5ca6l!9 z;FthmJM6OWv_DZlQ|Gj1w_;=%85)MWbMj~9pUXU-Df}z^Tl1&-kNCIbw>U$bQTV6u z&zjt|xo2Sis;H{0QZy-=n&6K0+VsX`g#5kv&_hY&_T@HXkaUynFxjC2glX{y;Z>?%qHB?R{4Fju(ito4kQZ{Qog zs?t@TIzM*?cLa4@Y`fU@uI+srvWDh$<#$ccjnjQGeKGmk{A@RzH=K0HB_B*0LefEo z&Kmk^`YQSw`gf?w5a6?xyOz5bY8!hiTh3}`m9s0^PXM+%k1?Nt?B_l4J@IkiDc)n< zWqoUSZ$KD5Sx_UOX6t6@;(A&%nMRus&3c+`qfG?!R1r}KT!-1T&CJcr zC{7f|gYU_25;clq!Ow#D9CYqFcNz?zjz|tkc0oSH4B>R4iEUtyr%a+u0Z-8n`wzRH z!QU`iIYzmnd3p28+LyJ`N;jB2?khQ!dpg%Y!!Kjy_m$sMf3Ev^_0N?*eRKNd{DJ2Y zTZOGk1fkGf*kgncLRdm7Aw2@E7EMGG1$YN|M+Qd*2Sf)(&qZvtu!6zKF~d~Wn|0gnlm+dCAlSs@(<_t z&FPbKH}h_0{)PkpEAfTAx~f2eRcLg0xaq ztSau6*DLQ-_NnZFIRkUvl2H-^>N)z}N5mHjOGEP4#( z4gHsOUTeJo8x@R+#73qAwie;O^$G49>>lV5ILUv4|4gr0UK@lPgosy5NDvX0pckW0 z+E3X}>rd&66h(>=@{#h#jgJ~9*H5X>to~cQy?k3a()-|o&(l<8s*0?Qs68z^Biqrm zvk7F#9iKEGHR~{7INs;7`^pG^bEf9zmXWW$1PAPI} zGr*Y};Y17JgN)?ht);A`SnwA76W0^hLhC~74&!#?6U|c%k||Z%THLxndxSGtD_3{NuHRT7rtErd7f7pTo`JviKZ6Lr4?C z(k7k;_b7^#Bn<+V(q!-eas(W~CGHh&1S_2N5$=Auq&(7kY#R2l^RaW5ZI+D&XOGE- zNrt!jxB9<^zlH$NpKyVh0;W_W%|talvp%!p!0$kHQC+W{ul~DxzQ@1A$53J^6|@SP z7G{<2Ssz(TIZHY9Tp98ba_4jAaUfotd6asTDke&ZpRgaXn_z}M1=yWArW_NJ$A|FA zFLhk**dyO9ztV7}p{b#{fdm`N%hFe+SF$f>=jP|;tD!Fz4E^WehLDB_wGV1b6(x#t zIN{WhW#l=;Im89{fADGKG&1sU*@X^aqE}z9%|4rbvfZ=YN4br3Lv;N%@bQmwAL(Aq zE@mU|gB)rxeF^;x=`<;d3T^FkVr$*TMUKVe-F%7ibb3SlxgnQsh#tOzg_5=1HPBP~S6w>D|5abE6 zc-g!e^cnPt`0;pz;Sh)m!XfAEWLJ`_1Nu(opgquAbpLUW%GBlRy`6hHufn~$s=cOt zx?(CMS6zVR#Yb_i^GfGr^#XZG?lum|?+gOp?_|j4%7K6H z2S0J#cCYCdj0 zPE3(dhBAgS4l)lggIOUg2iwlx#of)#=jL%IawfpNIg7EGx{-PXZXwUe|B}ame$9rq zqenPLI2YTN*e;qb!aQ)ec1+jUuCeW-+mZbC%MF(sDjO>slVnMZ2N>8z#3-X+>uBd?(*oi>bq0LSH}^ql?fE$WXO%%$!`eJhx%q!@Y-k z5AojYwFN#On|xeg+%@_beWKl?-Br+^1^X9Z!&}7H1lI%;nUk2qi6ePmrJVZ=*0t7CRYSbJzY*vS+RRS#8*b&GWpgTycbdV96- z=OQp+m~q5$#ClQ#X%x&3X5nY!TX4K65{Dr*Wonv`j6tfL2KBNA5>1Aruon;y&TR;I+H#yzE?TU1GHx?Z%6s#pr^4 z)5wy?IcV9Y@|0{zo%=xyl9)+yGDx=XqXs*5T>0(Cb+kIKKxzv~Wo zj88P5Z2m0&B+qNlYd0#43U8&i5}Dg%=`-~!Y%6S)a7Rl)r=VGACb|q=jz;)tyKp;j zAF!XWt>_kX0LCA)3N+%#oZ6SDU6So&;P1ZY6mY84q)C5l75XfsISF%YCN%6X_Fao2XsnCG-*^cTO!!$3k+a^mrW}(M}@zfCcsi z_TAP!)_iN8bueUw6U=yXtFgtn&%EFK-TKYC%(m3#vZ3r2r^R_3a}4ti{{g>-yqcUx z%cYHDPGEMkx>yaIM$R|z;H3#ug_{Li!AD-fRRibsFzX0w2zV%ZQhHOy;>Y4QqBo$G zjxNV;=!JpBTL;+xPJg+-{C3^#x|=mOYJS%Is!1*#SnBxU_;Kd#`M2GWA%8FLUS2^# zQ9%}%eS81y`*)>mxvb65WM99jGjo20UFL6qHtmOd&tx(^Dg&J5cUwBg{(6Sz;0lSOK`vsN&>xw zw9~D#hay4oto3PYM&qx>Tkt)`)x-kzy{6`P-Kn}gb-U_r)ZVCFQNOBwC!7WCN{14s z!D*)Gr|K60`(`=J_okqxp|)T*V=qJ9L*GYVLR~@yVEwUJB8~|1K5!HS5qpw)kr0Cy za%Xu0cK{m%zny?iz-A~!qwoT%!By|7c2qkCz}k&y-@angu`+xeo&b8r5^^yaVR*E$ z+t@$(Klxy_6G3}d*k70=j025nxwt}%=%-5|pri|Qr>%TBKa?NL?}X3KVc<$+kbnQz zQqpkdQbfU{XzXO<1k~Qxvx3Xl4YR*XZDHx zv3)0MCki4c@PM=@-vWj+TsxUEc0Kz$?;G!&;GCdbSRp(jIxJ#Btre^jAbaaT?m+Hn z_9%82^hqL#5yYY3onZp(iMeY+xWNf3m}(r6Z8oRVG4d50_1u@ zHbD=_bVYQ2EHZ~Y3_leAul>KmB|8S8rPb4RtQ^}}hG}7v6Oe%}IAG8%6m<|l$)kXBS$m?|krYr3bAn^^f z1Szrv51DU!cj?yfD8oS30YmqX;7j3)luEsQmH-#VaJLu={7T}fy&)6*PY%YhvW-P%k!Sf&!JW?I0 z)^un(kj&zF4GS7Pt39e;mcA@4EGjI*7vqXSvW*;NixZ3b7a2;lB}w4nxL5z6KCU^s zd1mXZ)&ZUUI}u*vDD!CZ7u#oB7vxuNg0-LzzBhga=-yv5Uo(4gdj97597Ka+4q{$~oaIVpIXEHWnIj=v`T+F+HIb4CitX`~OXSPsB0?cy z3(T7jVGdv-p}+nTbl}7EDSB0xsw-3xrdZH6zs=EXZ?2S8%i1bhDuxw}C@Rmd$RA%Y zw%~5g{hXiQettXj;n0We8QU_D;XrtOM15O%OZnoGr6s&peru7dz~x2pqEzE+@kcO+ zF>Bz?w@$iF`ZV-$Xf1?0rA4Jh$wTF#f+#`MyWVemXZFkN7n=~9Fgf~gFNhr7h+D)?4pG1gzTcM z;;fwoI}65^jw^MC`cU+~sIjD}WPR=WT7=iOSGh+yu6um<9L*dJ%|f-jgzO^$%mxtl z-)`zoY7(d>h+>ksk9$w|J#Ks5gc6}-091}!j$0&nCd0uy1F}tS94DT$pSzEX;-a}r z7)u!ANMlK}aIUEEc8F>fH||tvBB}#@zwFc z_0CleC!b;H5$M~%XT}1%0?}xkBc20_(P`o@$R6qkeT&27!{iLmoIRpEqIi(q$Y(%P z-$ZI80pfxD25L8P7g3GV;xITAZVG-1KAIRytOQ-sVtAIW;;-Nbk_M8hDK$VVN~Iv) z{VBw$L@$yTsS)lwGKvg&cPJa+*Rcc~!4vNVr}No(q~9QSwm8Sb+KMnw*Xq{jrhq4) z9hkTh$R%h8E@+{wKt`)&)KV%bmB=%BBJX70Rp9@f%sZ9$IO9>qnony!olZZUUY}c+ z>t7vIO{peVXH@;FdJZ1jU~~|AE^RJNL)X&pu>&tK4#8iB+J)cw-ih|5T zo=bqtyMld)b%=#%flm;R5z%-I9^wqZOFI;!N9j@Ppz2(8 zE`O)5bC-RuJqWT(#7?nOW^b^w;QUc(tNQQmq{65%QP@Z&hggU}I!dn7J^zd_VvvE!cxms*rWPfOWV4iK71Jm#0rfNftp-J1M^;i3;uPU!8 zneEK>vrT84eCvGc#=&fASLyCjWG{huj^Z69J4)(dPBFM_NZF^_kF|;pMaLE0Rh`vr zG5@svvQ?q0(RK7%dM>c8kX+84;$7l}ZVTN~eb@OaLzE#Ek(H5`qc2CtL`6p}h*%JD zGyHbAG)x*68Wa-5@L~GAki3+v=caOhKwl17vv)doIu9ET8=k74s3j`NfBk4VaP5vY z9Ba5(cd715?d96XHIHlH<*51zzEBg?kh+w*oAMj-V;#pjs+3jA^O|!SwwYu8;QZip zVjP$}Tt2QBp%`eA@#&SkJuxFHbCH^|@I_@Rz6>cDQAoe%vH!9Q>3Om*o*KXi8Ediy*C+IY+ zbgpzFUVgHZ>`Zf|x$ICUVTPWJN=A*uj>P(r{74nl3aX2SqO0lM^f3^se3x^VGl-ka zea(5t*~i|^?#1fOTEkq+e8{{H1ctTDBf!b_q+ZUJo{(sXR9~#fsmQ}X<#rwG_twCm~T|;)q0Iq zqkRVR2#6{}q=2f2-H*CSDx!+0CaF82MgX6arQvFP)L!Zp-7C8HcOB@u1Ke*`CkyTk zJrodg)rs^OmUk}eyxexB?VpYX9US=mjWErt0T##``y2aS)Lv8=C7d#wHI7PUR zw~vf*H~$8oMxQL7Y#&2_F5qVHt>Ezi;{!T9Iz5oU(#hP( z+@9C=tL4hdZjQ+I#lKqlB4w#fH;fbCJK~W%uX3XHs zPfIxPt#1(Jk&qX#~Blh*~UyG z!dDvNNO9~0mP$LRgT$t>Xj1Ue9A}?kCkPS*TO^w$^Q3d7VzESgS$GM2^KV6w(kN-I zxK_-D-s)7&6wYGqA}&Ed5+G;IJZ3I)D|IXN0_c0%NeU7V&bkOkaXrjZ_JO7cdCqR@ z?|?-nLf2Q-SGA#YBPbldb*}DS)BRNSOvUbIb-(F+(|HhTP1l;PSkU?4)HpR>gV%i0 zebg;5{bPCxT0gN>WIbs;WxWR;g~`rI&Jm80jzPA8wy&0NmYe2V=FOIk7M_i7b6H%L zy^v?|Rr^(IS6kJH-}AERvgwTJvR?^kbct)$i4oC`+>7V)#7XL2Pp?B z{n`E5efWL&=fP*RTChssfL>~pG#ZE@Z>8K-s z?49g4Feg00JIRX%=3WXg?nw+XV+d^st&Utr#sVXJ1e__5d*Dp_bo)1$fhx?MX0@@~ zxWur`(5P?F=jn6x5r#;^RO1xm7gM@vw`I3wxP7?Y2hN|lxLjN!Ig#v5_ofeJ4`W~B zUFE$NycSTz6fyF7_zQZvy^_6>q2d&Amslw-k(5K{1S>(zs!{xCehaWYQ$h20n|_Oq zreG+?fNd`We}9ZU+TP3D(|lHcK_91%R=0JwcOt#ytk%D+p{-%9<6FkJypz9|e}K}p zYFoD`HY=7Xmn+YAo$tD$x}sXAOV!;q-8P|Y7#qn&bZx+Hz#@G`MAIQ;h#1e=&)J9u zkPUtd16vO>!6)pyP{vHyXN?iBM>^GFS>K<&fZ)0)>e&uE&_ zl+cjSkWrga+fdb5wX}L^HB-irEvsLSNYNVZx7}~Ec3Zo9XySpIe@c^NPBJ4}g_)!o zB&5d=j}vQ?V58ue^n^6fBf#Ud=PA!p&oa;cz<(vffw-4vFVAp~FpuxjpHi_A2~*@1T8XHZ~g(4(v&o4NLKr(A; zTL%1s5JiZB+(qsZLPq!yIKP+cOZ9r4LAMcR+`X(lt>w;2=Qp?$4~Ngf1MEZWEaFV! zePBM`0qu(~>=V-oGYBv6|Kjfu?h=p;Qx1Vc*o57LMYt`nlwfv1KduYig*LbhE-`Rf zkuyOG`0IT@TLVXAQY)#AbQk`%^Ps(ZO?^!r3;V|^m^@V#P4LQT^!|%gOFcOR$!fFf-y(80>X-l=FTBJs) zQG`ruVbC`cd=DQ~A61@R?p;F^DT+58Z#qDNt~}R$t~*c_sLF*J2w5lyv$w%gZ^^Oe z+Bdj1x{}dJXcN>i+%X(-r~eANFT|gY-02rE=QBS6tMxDIH){#ZLoz8@lvSkVq&L9D z@B$WSqq)&MS3h6x4f|8QqF!N=o8*c0ed|MNLuxCkDykM#%&&-rimHsNd{F(My1A~o z?pgh_dZ}D0k5$HkUiqW;nf|GM40z_o1IH$a6hvac`|jj9dFzGih4pSSw`iXjABe5@ zTLl&47wz}Q_m8jD+wQ&2dyTi$$IS<#;yn+G4~s{0MspT27BbdT)2I(1SoWdok?XYi zlsP~js2{3H(VXl)-ff0l#UJu-^5mxErV&jen`}^pP;t%i&4uzp`Qz3{t-KCyM{#Fy z=P6+KG<4N>?eE^-y-u@Ev(UKExE*F5*X-BqgW>*(+yTzOO(X!$uxr7qg3N!B*{h9V zWgr^!E$l7qf8gFffSm*@;zxE9yOBMNHJp{g7|gK1+Nh)HY01FUTS8kzQ&PGp?@1p> zazZmmUi;-2GPVP9g2XfpbZ9r})5MWrPTdnKX zbZctC&@fFiRWnF8Sl6m=11k3}{UzOHU9-MPA8ZaazqG!z4uv%VVZY7<=FMr`IUJEd zAOw;ENRNOkPz-~ z|3QBSR)|rZqdK2KPC7EryI6g(I-)qTI03ROw3(XBJovg(aHn7dWL&`Gl6wd;1g}8; zmatXWI=6jZ`%v{z^%Cg$r=io(v&pl`55ecl^kjOn{Mdd-lOiuXFFY(RJgy~99`{eo zf|vm@Nii?uUdEk^I~(^R>Sa^{WHS!+AL{?a(~A!m?JpW$I+EZ>>#z&R3&=ufP9j+&1QAY@EEyyj3OYuIr^9od z*E%nkhtp%Z`*L@XL%1P5^#E~@c!XfMAO-eSBh$iMMq5IgPo77PBt{bpaCx{Mm;?;c zr%eIg*jCiG|9X47oqLeDO6MxnO4M`c={jJ3wg$5nBg6@DRS;B)WFy@MUR)yZMv>kf z!U)+0Ue_i{BRC%qP?|yK>J3>GYLW(sR9i@gfl1vCx`KD;_h{t(8|4_`*l68sEj5*x z(hZ*t7xkC*G5TozT;R+3>HYN`FemK`I^?N_sfJoptqE@>SoeT-^|}3p9ez9~2|kC= zF2f=@9i2oa@f7(K`7r2g6G2n`koxey+23aJX7YH_1kyqLetaP4vRl!u=sm98u9LQt zHo$=!rmLo_5|w?GW$oqd$X<9+cJV((&9IW85E;>0mM6=VjfAJWrmnUwwK}!h&}e8( z)23?6kvv!cx6U)nGyDP_%3R7^N-uCS9?(oj=|Lxxr*b)e~@&Kc^ z$@`(l1CMUEZZ`-AlkoU_{zT?PW;vyt(hGY3{edm}1agD!JMKC@K&}ec!m}X!D+46v*@=Ow-_IRTS8|r7(-Zt zSra*vI6I*nEIaEn<1+){@MY5f(p%~B|2TCPf|;-s`g$`lvoMFyhtT(cnS-zpk+pQ9 zVWJ`05M?+B{eCbW>;47*bP??B6M>z9aLYPv3h-gvw10+fklLko6+z4;a?e8kPCFE` zHr@bE^-t^F@!jeyHi59u1xg=l>5%kRt6 z8d4idYRhViD+?=Mm%J_sFN!FdSw6EI$zU+l>+6#m1~&9idyXMwRnaX~o&SpoVGL&%osEzusap0RVHW<|{mn-KERL5>i|blWr=%fhzALk`Og<5i$b zE;24PE-_9wO*1{PJhHsEzq1EHM$`i6)`sE2aXSgy309(=i0DF@G#U-zTF(de&U2_O z%&ko1O!yLbaIb+EEe7bIKi!YMlCp{tO^71gfV=>!)8SM)Ivi8s-_3?wnjf(3O0{L$ z^ZK*;L59JGf%<`Zl}@c&4tW;_lg@P8de=J6KGrU9@Ejm@biQ)Ec12;LF)Ta-j{*+s z449FAA$}qvOr>UW3ptn)L|IH)OzOmU;!|)bIHVVi_!s$*<&bO3v7NP?wI~hUhIDnh zdbD!1GExz#Sk}I@J*p+DWmEm8dU>V1(!I>R45Ypl{bY%MNdN=lJlr~Bl+3oE1 zwYs%Bp8QJx9xtNZq7X0K4pjERPMIn?2|H&+)$$d?|Q*(DpbJ23i(xvWF?^Etqf;7Gp@jsk} z**8cl>z-CUt@;AJ-*I*0>f9RKV2neCRKc?i8(P-4yi~qYBK?18N$BsuogohPy?S8V zB6s@?n9nF^3fcHyJ7#DhiMWNqa%fhZ(>=@qY0r@ksGS;C0^M-{OY>A4x~k z(q55Yk@pey5wP$@N6}DSFoRtHjG{H*Gmd~fQasGqTVUpU$aKgAM;Xgf&38FM%+@UC+H{Wv5qm0K9)X~SW~>oZnQy^sL^m(cT@*knr6FtoBE3;T@$B^ z)m_)$(0dp?j2q1x%#q-tNOlZ#RKT5}KPnNG2)d;Bs3*71+)dWQI$01z|Q2(eNfGs zCe3ZjZOdoJ7f{`~?8v#@7v+bl!c<`XU^B5{z+YJeXGjfQL+?-TM=yuz#s={9!V=FT zv&n2Wlg&KHI>-{SMQjw)#h489F*R9D-b>s={0^+YH-y)OE}RMnz#$AWeEaJ9?h?9q zuC0#kj&@tS?V9!b9c&^K5Eb-{1}l5Qp# zFrY)5q@MsZ@%?&)cRSxW-v~%CQ=l!t*3Z@7g`81aA%JKDX#=T>QboxE_QMLsG6svu zW2#|BI0fcOTUpy!2ig082IK}zzK6^l&<4F{d;r=-AmcDFIo3f}{u%fjtT-DEY?atP z=ssvPu(0M?=UaIej^%>oB7!}#yoIywIn#MlmAT5i*s|Diz_Q;GW(~JSfEr@EeY^dx zu@uK;H`Iz;%)c_)vajr40HR!eIPK*MxA9RFZ0mCBR(ofjW*vHsM z*a<)lo#mS8O1352R+?6rl8i~lQbW06hi01w$=zvgmNz532Pf2xrkhO}%^A%@A-|wi z-X^EF(ponGFF>JEsu()DZiZ&2=9K!RI@l0mnCYD9#KVkZ9I$}N3S>|gyqOJ@NmRoS-jnRBLR8&NJWtp6})Q`Mw~CxO2|)+;Ii?E`4(P zwalydrq#_JlRY-OR!*&)E6%Ge#vkm-tZ$1RUr8kZKE9xL82L+nHB!aECDf|gC>8VLu!min>UimlQO^h~3e5jG1q z4NI2%hTw)^|KJx)&YA?VdI;9WV{tOhEq_L?MP`tx($LVzP{drs+{D_%I>t8ICb?Q& zV!OmXk9!eUCBABW4je~Yygj}m8lfqPQ*m%RkvJ7T(EPZCal2!7$9%(nsS5o6!|Gu* z0aj1RNXf{9@WZe@WDotw6DwTgJMKGfx6AE%GvyDZ~MY;J- zm=n+dC7be?&Kv(QV!Lbl!r0$f9IcRO(?s_=%0JS7#B6}xd?Qe+3 z-7EaB#N*^9ob=7_x4h5&F6rIm^r`7NxgrVSghfDh%jXj>BnIex z2FCP{*}`0AHM>2T@$NJ1HxQqpdl-5Ux*EI^6rFqdaM|#q(4)|C_SrSbkNwpDsbBoN z?t1QcR=QWZ#ecR4oaW8$O>WVpqhEF>2K+8>lf-5 z=;!Lc*Nfk33-wdp!hl-YP}z`ZNHF}Z{-ZiomzoP*!~xR*(;#v)&Y8}bUURO^r)PSO z_r)#qZSx_{*0H8BrhVwLOje6kJS@K>^JFfX+(V#gOp};2dzw89+yv8rXxUB!eXJgL z&JSYC#gvWdZSRdLe~SG(5>pOXf3Zp!V^>2LLmBqmi*$>1&Dn_W2yYJ$2@VbtQSbko z#f;zM_e^n5abI#?#4_cjqYGG@vo_~^_Icb9T4f&tpX4;n*^sj_#|Z~+6JMD>y?=W5 z`hNBe2n-D5*9vH;29=|RBZhM9?Z;ZiqKTYmFCJGa?nwNR_#p{{6J{pNO5n?pP%ge) z{Ft~gaaF;w*kiF_46>#grx@dP3A#(6OQGKWp8iohufl_y{(kEF^w*iMmp)(iJTC*U z%b@vx&HoEN4m=(JHay+%^yRac&m`CTqu146OU7u;cQxNlNdGoH*_G_-iAT^pWv(&| zFVH*i0N>JW)I*1HBKbt}U-|yb2jQ?#|DpqmCKOL7{%gqtCA*aBRw}kcY>A;{{(M}# zcJb@Q{wgMX*ZIth@{o;@Hz{9|?5L+QtAD}4{x&N@0XFbvgCc)_hsH=U7ET$Wl2hGsy($VibT&}XYdclEwo0N*$qH5BA@SQ5LJ6)W#Bxwb>pLj1Zjtic*C1El8b0tn8Pvnt( zk#(`Pg}H^fy0N-Z&*s~v+SKFv-}P4JyDsL3_k#C=G6O9dD9onlHI>+ifCE3L<-QfZ zL;gelnSq&Xwm%EB0Z+gJ?zN_&rlAt#o2&`{$cer%yiMD#U4)PNTK`%<26WYR)iu^N z(dE_W;WoUi->d9XTC1(pAJw(0&Ja=i8TuJ=;{!a_IL6o)R0P-1JFhaVgdN8$5!S)&ibDHsa2BjD!cc2DrovASpX3`{#^38I#{lerF)N zaQ@5rFAu#q$O@e?ZBYLwQaoyvD$MkvL&+}FyD|RM2r~ZZe7j9Af)8Z{lwGw-#(z#0I zF7>kbi{ePZi#93TwD7b-QwkL6@o-mZdIB z{rUaR?_)D!GX90|{vn<7b+Rm%1b+w?h~$rKSGTLPsOOg3RzzzTbAl=1S<-(=tMFZz zi^AKI&kQX2`haElpRCTiCU4#3I>~KwwaqmuX>?K(W|YO_i^UI*86I=Pdc(Q{-j|6# z_yofQLx1H9<$mOTWN>(JxO1p83JEk4;643$C$JeT4lNGV3)iPpKM`)KHN}n8tM%6P z(X~<9D0+AoE`!^U-FMB^;Ox-MP)ay8 zEIZgA@!2h|7FUNGhKm)fF~#)O6labz%h@S@pg$RZGETsU+e3Dd%$Ur2lm49chc?SU z$DfV2D+Zcb`|wo|&)`?&-n!8%HpX|O@zcgn$EA!*nUgv<_5QPa&pv!q?a}xD&iVI2 z>VwprOlM|`^ycY%-tB#tHzRLGNl!_S7Z1Vh#_h&Mro}h`QeoK)wu4E(CjEOkS!{Gtl5&Q|*AxuaoES zS>$;nS)VLcwE9iDjnU^tYM%O915N6=eAGbFSEl+?*=GLhUlmvxFb6HcJ%OJCqETpy zrga&6_#E(!e;l`EP28>r3)aIT`pEF1AzRH+Q$RI#&tvuD^zEtR?^B<|24Vu0e3g9xPr%c_ zRnN7I8R5>f9cee;-Fnw8wQK6*H;>=Ed71KZ$MbE^XZ||V}Z&LQZ8*6Y{n#nU+hPv6BL z(UxTE9p5W{0sh4;lA9-s&Z2(q`neZy7ECNKvB1KD3k!zwhw|TqZ_zEedvfc%t@AF* zw=kb4w=ehb_^;wW$G_q;`{(u@yqGsPCxnC+AM* zY&?UDWfsnyfagn0dTe@$_r=~PrY5D{CBs)dfh(j{Nc%VaUotOZG6)aLc%J>cb_)mmmV z6A@0rW5Z*^EOnMz3cY@XNX1BNt&P@~n&1`pZyU5Al7aS}b25LTYjlP?BxaIX*FD^a zCOJAgJ3BAAFS%#K8@TSh!RZ(0{Rcf>mM_OwKUhEbF%X^h6#o?eDgQ}-(?HX}PyStg z@rwA1U%%P2*&{jG+0Gp2S+sWJGR9?yhG=5?q;yY~C+n~5zp{njcqIL3I-`KBEso8O zKV5&p&RFhBccr;{;H5PPALXIiP;H<(NY$A&QzkT(EBIuL!?So>!nOoAI?wOWoUTe* zmGm_EadNqQ<@43R3*mj9v^-mLZ_T|YX?Ic(&CgmeBz|aoGkY`pB+Ep$NM$VN&3~Ae z<6U_L{$|y1m2hW&7k?gKGKt0Gee1m&y`Q+7xEGPRC7;(QGE*+%>9g3e$RV|BWAKsV zBS&Lqfp6Sz+;cp$Jj=Z+y?yYr7e2dAr|5EYS-Klk^+*bpUk$$+3}&PGQ}naK6})P> zY}v|l-o@UAUQ`%z9V#rnq0jZWu06uQ*6w_N?`+ z^YjI=JWHSYn)~LF!}FM0?_}sis5P3a+1ecLagSC+UqpXiy`Xk6bmp%1Dd)j1!)^m5 zhN%U!56KG5rBBjds!}bM~dUeoYgt?GU{iDSI;T%-~0dG zPk%T4U5mG$zP*rgAtjO;NiC3`KfQnY!1RM@zos2wK4!z4a)D=_N6yfSx{A80N)@FS z{u3&kiQi&>i|v`vE1_bp3c0G~`6y5O{O$A0K>JvcV?~}8{kP~SfQ4GoLq!f1`MvP( zg+~`0U2uE8?fHt62X-{+SkmLT$JAy^?HA3L&C*NEATM=PU~FJ6oTj^E)*f;la-CsL zcsBEF<~R69G)QZZ*7kjy_qE^EfnopkyGCgsqe;e<%!`@l=%=hMy9*_aw+Sj04SMLGSBGV#TNDBq|b~1D-v^cz&$e@_; z`r!Ito?zZ!`B3@L+3?x$M)YGZ;Lh}jbdTl}b<=mld*&~a?=R>p!N)D3mSk&kTD`B{ zQQH|h82sP>J+}0`0}TTW-=Zn%3-7gMHf3{x*ZjemxgC8)=IdQ8{|^T%n|p%<*dn>jZ}ehmeTzQ*b|!zt;$S4`$=Nk_UtW zp@4WUcK{pEksbH{?!U#J3L9;o=m7742EK+qlh2HTN%LO8bFm`TVJGM3PFgP*UTd=R zfgR~P(w+G8{g?4y#$mLppSeGCzt2vk|80}K8vknXVJH_Y8@%be;k(CqFEii{ytk5# zyU4Uiq%Y?2rU_A9K`whP`v>&nZua)y$IOlSCU!z>E@qZJ61uVtnw5}0zCiru*iEtd z*<>7zKZ-(rVtkd@DzR-;V3#a({$3P!sY^cDQ!B2DtmX=X>URB#Wsj z-aBPNJ?OUiUtn7QPg` z6kNv+Mqb8fKbn;*t9yF)^t-8dQh$8? z-|~FD3-v0r7oUN?#rhR{UgUWZcOg%qqp-4juj)^`tO z%XiUt(RVj+ClE%TlOB2>60X4@-C$iwrG#QMm<=oOOEQ}*rd&Wgqe_$ee9m~**a#0H znV$(KWt1{n5q+K~;*EsD;c&Z9+tAYBvf#GBw!kIy%7;BiJf&EAt#hq))xl%@mHVYz z#f)hzx$;skHt;p@74#SMcLQ#4J$OC%x%RoXPPbk+nhngC?0^>X-cGYjwUn@xusyIp zw6}Z+5*buidPJH=w7{3d*W*MBC?@W_T zamIM#8|9TUn2ax?9knr`(V@F!EdA-b=968P-yK9Hm_mx!diUq9&s+x`2OX1hzRPK! z-99^iR(|-ryE4mVmCIU^wK!{d)`%#7Q*kH`uhZw8>YC#E#r3NTiLvKv-`74{z{VVA zI46=X&^Xj6^hx-W@UP)t!UeU$Oj@>*9P@xJVo&WLe{Tg^iHuNs=seuTYy4Rz`X>2O zy>GokT|-<&vWsNbAv0UPEzDS$k%lPY5AaD=v#jp;^&Z9}dwSZmw72PR(w{i~bF@VF zdf#*3GtoWCJ<2n}vn8-K&<0MNWC~5;Yk)PAp$~mdvDhN9kpz8WOrkweG9*4r{45bt zYwp>3=H|JP_j=x^d7k8{4L`ICIG^V{j@6lY&gVX#dvD^t#86x?ZVP$X9JRHkA5G=RYn({d_@_K43cegNhlQf8TZTq)N_bkhGyYPyeYboplLKXf zWrADSC*&pIs4M);jns#meVcu|y}P^>(G(7M40o)^UY`AwEbO*fZL%t5R?aMkUqVXnBT+zUQEm4SH#=n>~V|96OuckJv)kfXi*lJ z=bP)JSLtc&X%w#x(Fb4FE@`{aHYy=q$ia#3;j_Q!{{v>jBUntw@PaR50}P<9`k zId6~okNE!rLjywt2S7=142{G;_yb@agZEBF0Q-%j6Y z=V>P^lI%O#ceB5NtBNeo!9kLq@IL;1ee`HMGk0W`%`OWqKQ1#F5z$sSSSijoPCL1o z--O49ci}hY)H!rgFE_F^vK@~<4&SV7QXf2rrsSED=PJ3C&5JZEvcK5AV$D(VFD|yI zSeIg*i%l&)z4(_U2A3F9Y;3U=g;o~oo4Yz-N8w1}NLxIyCV0N_IQe!i=Z~D>S;MlD_;X!`7iM^`z8{}9KFyS2&Zq@Uz>(p~ zxB1*k)O0SfPekoi?YfIGF|(`kAwE9%6uKg8Q2Ln2}%`&NTeI=Ai~ z^OZeh3rz}s7wpe2wPm11KypM4K7;Qg&qp5d{%GrI=gAA2(32@(ntQ6dD1oTo;8D2^ zu1!U6MQ?jwd!KNoHwHEYq!w)tCnF0S3LgwtjZ}?@zG_LI?A~66Aho7w zjtdwHQn^$#TvRWrU#R`*-bdhw`B+`8u2Cg>?;k?gZQxYo6bg`vkdO$d_5SSh5hhf&M2ppEI5Lf%$Ll~EX^$6n7^iL{lmP}w#+8p z;u~T&#!B9LeQ=a*^?;-SNyBpu&viBFO46ak!Dtf*j z(Bm}InrV`2^kKMacxP~DuvnmYpr605zd5Muujl6&^>mFF*h;{KJ*+OE zKd+>(q;IKf8SPcqMAisxBXW-$)~mT;9EG*x+6hg3er0&N8l-@VJZs~1-{`97tKfb( zOWz6~&wc0-&l@fn1{w#Tlz(RIVd?=hXR)b+=`+(!<4t1$X5Nzw6PXYEs*0~mHGMVx z*XWWChYy9tn?QY&~-uz@sErFx5nKSQH`pNY6ne8+GaGWQ#^@78YY08v&ZFxs| z$A8{`y^DMcd?VeX-F3*DHw27PZFe1|jYwVrV#d-|ZL5|5 z^5c5=I99^ny9vHJ#qd+vtJ|lW2}*%xx=(bunJXXCAJm@!Gx?0G@>zG+_lR<6ovK5{ zeLy5mOsIp#vZivfA~6rMd}R^e?%yoGS!5Q>U2$D@I4e9hG=?rYcS!tXpezK5-SfX8&*m54r!dUT z^Ky@Tly|=OzW6|5-~j#emC%(?_i%SoKsNDaJP)tdR%-I!IZ>Gu<&WG{ZYjl;VoD;p zld(oSEa1P5^9&0Ne=85*E%oAOZ&8_F$F8t~zNgk*YZqz{YpXws@QhHC;3vT`-cjDe z&cjZ{VQ|!Q)Nyole&&1$ECiDOgrCi@>|xmxoD-ZY-K*T0t}NGe$90F?dk68VjrGU+ z+jEa1PYp>2-MGvzlWVd%S`Y-s&Gq3t0)RUGSR&Y-TmG{o;D2)6@R#8#T&MxeDteJ$ae&W% z95eXsoDXaD>-6G<(t$Zd8a=6KC%9~aZvrWS&jXzT>wIf{$2})JFX%OvfxYMvlAMXo z&vHJ`sg+$bTjryAnI&W~AZ?IUKg&y@wK{8cR>ACoR4Zk(8)Vndehm6L`Z-3qM#G0H z?->b~qXC(ZX~DGMT5YZNf((i^WTbRvXYVjMO%Vi&Ct^;<$oXGCz5%HdH{xeALl~Gi zI58&44imF#QtqT=_Ph5{)J{o!kKj)>O`YsPDlqry8R!`hJ@_&2@7@}o>K@^{ZE|jMPNM!3K3q>c(d#+uI~`!Cdx-m# z=aff$zP9?e`pwK6s#3fC!hM}hHrZfhh~k3rApX>%<;;!0YDs*5dVqdlq=OEv2RKo8^hjSQ+|SDdPk-$ghx2()$MBrtIpZ?NW?p!I z{{62&m!;3@O|4%o^TW&@WK}1<&-MOb#=#6m;G9N%b99-yOkTZAj94!!fH8S0z@;RXNwTq-{x;I8((3dS32% z>?a22>6xcT9%n9BuF8p(5-*WC)D2dW@S^v?{~67EMYtnRl&8vZ*jyWw^~xjsA^HXS zkstfFCkW3VpDVv>s$;5SP}V@Q=2Fu28M=%gGM8pb-y+bbnP$lP92~f33aNzUDpHfwz4PrpV#}e9d~*y3n@B zwg)9;e2STOTmBoSNBv`O|7nWXP?=YjNLJ?W&T4$eGL!xN|p+i<>+gUqNwg->TJ40 z-8%~{|12`nrlS3;YOHL0&C*|VYl=Z{sH4YZhsWv!(t$T8=be*{v~p0FIZ#M|PDU@2yMWqxJWxaIt&fazy4 zY3qXTnOmMWp6CAfC%Ug~#633MFx`NSYSha-N0+TD%&y?F?y_zx`{U1;WE#jJs0A{( z+X{yZg|`H^1mF1I_=T@_%6D316#g;((f*gdSH5}Pc`(_Edy{;*eBwFUhCa3#dx=bM zmbaS!Lw`mfBk(s${Au8I;AKGOMv~*)A3u;G%xC5UnW4^vtCym@RXVGk)NCb7xyoI$ zR9&JDB~SN}36B{3Jt~?ioBZ@YxA=Z1OBYK=YkO-a_IaNG;i`*Y!fe|t9B(Vx>Qaj* zTaqnL%}>oT1GsIugSs!j<+kaTNi%A=5v(_|RyT}75B7t8iT-olXF4~Uz?-00qVbn|!j zOJB8xI=7ToN^4K`wt${b?&CWkpE0ko0=wQV?9rQ06;%H3 zAI4UVl{_8M)fFOBa|~X$72?Xr4UPXY{%QOZ9DX*(_lWBeS0Sc+Og(!&`-qs4F-4hG zJ+(iv|86^hnqZV|ob_w#NHm;tjo(W&hI$8n-x_jrXTiz;SgWHQ4<8Q?W|mwySTrcU zxEuT%{gb?tyrO~ojC$vgD<>&uXwH{8pE{a5XtUYSc6R>Y zTI#Cet?d2I|DAts;QPP_!O~Gq!N$nONN2L<<@4)<4~pbT6tWev&5D^BBV6OTvERoY z!i)7R9==uLs>B)MmH6@mA@_;v9ak~7A~U28F(tuz`}!!~Vwq*BC6_rDJJS`$qwFfI zibc7lyR8$xZCSEg9)uo}PBJpo5Bw8+#3m>?n92Ji`^_#!Dh=Q%Y9H6{n%@?VprIIx3ssk8-RSD z(V+c9@T=`t+Z$^NOsWRfutlRX{>}2p{Ezuh({-H%haK%yWj7A{|Ww#!t3dj)j8{V&a<3L zu8Xdl-rHW8<-hU2_CFv`VOC^DM0iO9=`4Gj%RlbC8K1uQ_k zk7b4^9x7Ae|B2^-XyUh-wwp9l*mTYGCpXXcuuu-dvmIo9Vt&lzWxQGD#?`@5>fayP znP=cvCiw?nnTMJOur2=>$Qj)lzL9tZ{Q^>r@3_^%xTmKWe*hJLe1i^Pyh48QJ-Wxa z@O`$@H{{ai*4L(Qa5EEF7hcbUT7G**1al75mT`yeco#&nR9QPd! z9rYdKbH?Y09{YUOS^9)R*$=Ys!`rT!^I!I}?Ah5fVIOYH-UN3+G@{byuX3z(>;i3_ zt>HI}cFl#WvCX~Jz0tJ^_3{TU39I|gGldvC6OL$GyiM?wEgz`BM7RqYK1;9%pWUy) zL&0028=>52pIU*>w9iNq+N;Sda}wT;-*N|7K_y+K=)KZbX{%IFE2^c~{hHX{TrplX z7Jw!=g*$FOo=QWlL#!J>4SOxS__nN!S;2O$D{76aF>7Mi#*U2}8`mbjb-d(?H;!!- zyD?^C%eA0)ygVm3A5yZk%9Q_l+sr6 zey_yp1jy|Wl zqlROi^Lyt3HYt1zZt)B%N7Xq9{Y~L;k?=D*;=$-~=0)a5D(frje}d0;n>?Qia8D<( zZ)n6$^atwt-_Z0-HBUFMGOaRgHg1B|(UVH&9Q!P@X$RGgnP=of!$;ilBRHj!)Sux) zWsvE@XkIrqigCJaK)mWd0jsGX%L6%&MVoslii6zcCQ>dY=`VtZz^YiIJ^0_sL**v( z=XcZ*LyUuA3k}6p=ooimd1gr+O$|+rU|YnSuJRelJ>SUK7$?N)#v^dEmvUb0;=e0V zNm694D%`y^c9W;@E#}pylQRTmuEs~`4)c6+T7(PKIJ>b})nun+zsYu^K|uQCNOit-`si2QxL&!MxSP5~FHn%( zd>go!P{@37{y6r)wE~hKbrMEWB{VmoK#*y2%YeehUU-{*0=)wtX&-5yg*t`i`@V-w zcFcD>cq`g3kI}|z&zONE!3_(Cf@oqsi!7$L>ZkNo8sb;*hwhwit+G~;8tx-Yb<28k zHM*I)nrfkSO^(SO^Kb0Gxb`fFW!w$bKj0tX8}3{0S?_rVhpGA+YVKflU{ZVidB)O7Yc6SWBPt~&!2;a*C_&9yP!-ZHISVu0*tzah@0)M0T7Vpj- zWHAq;PK==X{y-^<;qV!~+!yDI_lc)%D-h4! zcgTOxe~x|HMUW0^@l1XSj3C3GA(0R`^mZkU&xkXRew>B z;Fr@ejF-*Il)kYB4NgV?<~1no6u8=ug(kW@_odmO&QzvFJ}^clnKRypeS) zo@CEC3c`l7Y*{{KR{B!h3?d;h20HkbW}`&Z=a-ZuM>RH76lP z0r%iVLDlxS_!bKeIQKJ6+Uz_7Cc^U)y_;wNn|PX_xVh_bc$}UJ%vH|egZ`QCbIycV zf9*i+KqEY&W6(VPj@O$9A!%$RC<91FCzrn1-}MpssM6Q0jA?O)A;znS!K z!dvSJoFQjuH8=t)@Nb*Qgrgb!ft8^ZIJH*_tqZLQ?GFDOeh7y^{NUe2-bOyuRo2ze z*VNxeSO1gx6A4n))CO?3B6^*&2NYI|uy=c^-h-bl-c{ucAe-ItB6?%i|3*&hm}rAJgBax!|PvjJY1O zDakZ@2jAoz-WDA|Gh;Jj6L><``Tf@E*6E(%XH^hP3r|N+ur4$UZqgF8maRbv{HSD} zF3-2je_Oggb;}<7ujyF!vc70 zUjS#p81RrDqBV8cZS9UGJ_<7LwBf@mB$ER885RYDczznP^;O|KJY%nMimWa1k)!1| zbTxD{NEY23(`-|1W;a1*F-t&2b4Bxb{3vBsF%xXS1Hoi7+D?NH?bYldThP|R*3$MJ zeh9a4_MHyI144d}UB+F;sd$@;j_sBDO4e%%)?YZQexYv)TI*XOqYdji>$>P75iP>% zPnQ-+!+jwul24akR|ZVMZN$z#<~$gOx5)2&{?CDdJ3!t=;p~6JOs^l{h=&)LSLwq} zyCE2jwoY~|IsA^Xkr>!y<_PpftuCI>1H%Ktflvsig`Yy>LSs>xtbn0(DO8xr(UR~F z;lf%WlBz@6Kj^a>>KfpB(n2>+_r2~1{SW%L%zLJ(Q`O1p6dW3ksfBoojv0P8te`*c zU~F$Z!pD}YgNDAzX;q4+zd29OL_#~(ptP%SD8FX zKX=M-+Rz=%%N_F_vm2ODj@PFz-heOHF>u~?(KgyX%AUfBw92~5nu2qb+z-8QE4a>Jsr(5Iz0?-$0nJvwYpbIp0~|2yVS!xjm&9mQ0bR z?13)BbrTPk4q!YUB~L&{Jew}_85N<{7Vc?#t(``YAt%<@$Ysz`*MVNq7R_2X$^Lvb zYOw0m(K5Hq#M4nSN;=|o)WO`|+z(d5GtkuXiDfkqkA_9yOZ*u|fcusQmU_(MN`qf5 zzgVhTKD0o#-;nS6%J2%dO3`SY+#O##nsoJy za#|s>Noh_0ZQ|k504!%RT#-?^APkGASaGZDS9rff+;|`;6{%Ud8 z8u6W<0dIYqWt*k7wKa~=YpnI~SN2+c)=QR)mhPZ2Xl`z1{>1bNyV1MGQeY=}C$d*R zLY~HOZI~9ocfW41Uhq16r$@fWsCD!Cs(Gt<54sP!7rPd@>Vm(}1JnVV9a|i=K^t%y zh_<7@^9$#-`np)0H_tcxuRE%Kg>F^w~QM+f+kp}Il5qc5W`*csVL=FUNF1NFdpe2pw2 zb7&a(5l;h8>9BeS-l2nP>u(qJ{Hf-zhT5s7zc*Nj_GUMpEl^whJ>bBcCWqk@`0Lk# ze+CzZ7KZwVzhFc1DxAo~TJ!+2D@lR}RtS6mlHdb>3^o9F#1$!`D-!k4-5J>tc}W!~ z*_18#+7$)YB3C0{gU*qzoD5?)b>~J#P}x|4@Chb>9pEmIA72Vz8&-djm{EovtJO+ z;RrlLWZr#%USOTFPWgk`*%{qgospU#%$+B^r5C~HDA!N026WOrS_6kUe_w**V6$&i zl=m*&_sYI1C_8>6Re3vGlUKf9{J;251Wp9T1;?^o*$R7j7rWakp^D*3M0@zdqqGsE zTK}raXO#j(qaoZE;k3%UqK>`}=kr~?=sYhf7ZiqyQQe(*vlqdyd#pM}ZE0x5wjp9D z$$MIkne9PlY1?_q`{R8jdYH1HmANG;+9~Gc?3^xHu289c!wFpl_DDk}$D^V@Zo=7~ ziSO4U&>BeY%Pi9@(>I*d?F?-VbMU*!4`wMdmD~Dz`hNPp`cz#is^U*{OCk#+nx>DO zU_;Vf>#CihXPLtOM`pxpf$SEyQhf`jS$2|DNXU_AR_>uUy4Sk(`VF$~)i+cckmm6t zC7&nCu@fKqCF-K6zF+#|5$Hun@HT&A7>B~Ltl=;|(ZletvQXPjRi-FRHK~$X>rdky z84r5l#c`dy-uc=Bk`?=E()-N><$*l67lF*-}bXw~#}HD)U(-kmdU* z_%xUiOb>Q|e-Y2$aRwad6`%Th&|lVv6ZNBSjV~9eTf!wQ=q=>E;k!w~Ph($xPXSLI z*g%)u7u}D^?(|Zh{^Z_G!cYPCUiTjNe$N3#X;`AhhZF=JWhTkmVaIX?@Z>uYEX z{Gfr>Q2RCVYvdLhsZr`kb&z4OA(=hmHSmynTRd-EprEC&r4gTKC$Nyad4zSCwG-KX zSFD$<%gM?r3QAc@TGE-u&S6tq7cZ)FROT;@FPXGfH10F}Y`D!F;Q;fqJ^DQ)L>1GA zbs;7fKcVm`uS<_)(3_-CEq9D~!rpLd=xwx*8bE%G6xxk}4dR2;AiIHRNJRf8yA7Gm z&OtwND0mP@h^xW2%xm+8^Mzl4g?u(A&@dbU!ao$xyqo;D#;UQ*+n1?%@W<;wrFhCOXZhaxy)}+KcwexAY^oFB7C2}7!&V#gwDz=Kuw1ZwZ<#|?{;j1q zT+%Faw)sd#*m?DT+}iDif80_<3gQ9nsoIc4B4pALxJ5?bL1O zuUO0~Ka!<9pDx9C=~?k%^d-aI+DST{cr7|_FMRfJPM)7fyvRF;|uUk zZ=uUM?K$Jg!~8pfalsS!Q_}v9xW#wqhU13gJI7?l7-nbNT-$J1I?u+vE>5qXx~kya zyTG%+lZjuzb>DSgL*_RXnV07TbAorncfu)JiuO5Ane<|_^t1K%l)H-XROJ5t3{Uw2 z=KLs;mzz_`gWtr~tCF=6sWUaKnk8asK<(ZLk1DI(W|#YXvTcfOkZmw;K_|ACi)_0= zjLmK<2v4b=rJiLkTZvLYX1L-PQjA)3x;j%8En++L@hSRLy<{K%rTdf5;6P*!%DbKT z5C={9rIfr;LIxH62X@h;^ueaaEE*2hTOxWsdHS~*Gb#rGr#c2sr)(a?9p z7i}DK#BtmOGAEFJOXhx^`TJ9t>uqPQCq2p`wBl`yZQ1PgVw(8>5Wf2sc+0{&dj=lz zy|Z90_i4N_!MN106!mmfHbGVcwsnRLOc_&@6s7~im}n;{7tyHI)z{NYUHpPM=a9%y zCTt%^s=(tzN))-lUhDuj+)U69tO3G%c?qPZSV=vRqP^AHM><5NL?+`IKPl2T(l62! z7T^bX)y2@0#{mO<%N6Y+s+QT>La+<3LV1SIpr0!XL~o&n4V)z=Y}uBkH~<2b2Xz zc{eIk*EICj_b$Wxc87nbzc)Ov9PX0;g3p3=!XJlgvGI0cLUAE-A+kxgS(jVMjb)cd zJq6e4PquC1XTJ}ev7E8=xAv#18EtK5ZO$}dGMwZa)_8b&18oCrQoQ*wYV2hB%o4`q zZ=rdC`6d3fc9YHYsj)e`iQmNPz#!gQ^U$U3p~I1xLQn7;=kUkyLTZD;V19T(_zheL znVEf#$ApV|s3*XtJn$^=9Hw3hd2*Y$wVJR~C<9-+3`!7l;1W~WE&k0YAdW@nyP3>q zuQQj`U^K|D?F1KLT3~wgpC2Eb5OfDULDBHYY)ooerZKQ_zmCM?Q&WHpBD-Q^`~0mU zwY5XZQDX6@sKR7iJPO*At@Z>AWVbjEd`>;ym#%dv$Pc!dwy>$`XzC6JxF)z_yke{k zPLfkM*YLfe3Xn{iifA~+D^8y4u0XW2qO-#!MK_k|nAF8Pz@x}xZsddfcsTX)JSKsE zMWFXa_rd(+15^3g0R#oE~0=V(Cx#!j(a(P-)l)^+I>qU(2rg z6uH*h* zd-~Dg(cv|GEmon?J_iSEDcYmkc;1d@<6VZ^c``H1Bl^So`btBkJp1fnXw6C*OB!XS zzZRa2=wTNc7e>#yU#O59fG24FFBvYQjyY-g65OHok(y&PpMPCo0#o>XQ^*DArR%BN z%jcA(Wiv%-uT4ZpJcc=#)bkC|j8x>SRh0eG3mkjSf}?>W{1>f52{bg27s!6(u>Uu< z7mxj)@!ZR|_3#eJ?8K zNw7g$*z?&7*w5O|z~OCWJ7PU%ealQMY}RlM2%Cz6-rNtL;@3Ae4Boo z{)z4}Z-GHK2Y$Jm<+{wdrqhen0OQE@*oMc|BoH4?2tNru4mAunr25~>ZTbI5VE(d# zXD=y|z?(EW@(b^eH{ywA2i1iW{fq8^?hmj5-;h1J-OMrV@X9+y-@&80Bf6i#ZXorL za0pLCPLRaAKeC;j>U#J>v*}wuXBSyEQjYh>p*BdGs-lKq6j=_23XzvwUQE zXntVs3i5(nc+q`8wJn@z@$MD9Ymnt!C3dZ%pLk%nZwSMRk!N!|sE#Ig1UgSQ=2}p3 zlrdn2GF{OrdPTgzWxjkE9KjoA9vGz`$;9`xUhchK^3P;{Bpl*`kpi$O1~P3N$J6(5w7xNL zQ^>On%~|)G?l9ZF%~5Zz75WwW{rY{lBdyl&ATRPBNCPkRFQS};fAC>?s(-@mbdg`T zUw;$40jKmQal@NOr86Z{1<$Gpcwp#-R>}-$LsPA^?vEC zM@=){`wh-4gW%bwk-Bx#!>jF)9O}286sF#fJ#D?Mu&7W`FAiiH)W_G8&G2*If4*D1 zM|auswn90d3>&U28q%vwx;BK?hoqLv3FU-XrP8|`4=>RcYbG`m712od2U+mnPwGzU z2I&X#HMtO-`@LY_F_ZoHKWx@z*xbs{hEx8cp`Nj>@i&+oQY*YMy@sze#Qc_-NLx!A zy2I}+uSq8BYwgQKbS;kB1FQ^^tk5Ye*s`&!xgF)%?!e_kxQa4YX>4v}7JtFYAPyws z+xW)#8n$E;qda4e_!BGuCE2|T4=V{4$YnfKwgSCNd? zKGHJMg41a#^SE);(={V+(3Q!zaa2UIcNJaKmdLi~`8h{7CwdO#W0xm)VGs6q$3Tug z3sqW{zA)Xa%(cX~?H+d=AEb&zQhf_j!8!DcvOE17&)P*KXZ=F|FS?W;=wZe8OL#OR zKwhwxfA4eZ5g&7j+SIGgkSoOH5oRqFo`PrV8(3=Ffe^*kgNgXp~gF-ozfbz<<4TJev~(%Ki<@`yDEw%MmYGQO%QIv4Dvig^IsY?0`);0xXpf4 zGHHe5HbEIr`bTYLH}7i(K7PHZh>#dYc7X!SMF(mFv>fI?d-?Zw29h7vH`GU{FEE$? ziS7nxvwxK5us(Nscib=JZqE#4;ixnkH@?DDtxc(71_lOzKA=t3mv;0FCgBz#N<-?o*nD1JX;%S-&Q@ z7QMsT;Pl{(;MCxhsQ<3K7e;R4L|d1%n($%smUNwmx~GGFS^Cd8NOq7COf*-oR72dBdy9A|1&5AH+_eB zr@1}*d6&g$X=`l@_v}lisLOG>zGgjRJxe0-NUPCmvi@TEm2Ku!oHPblKDD%9&XmR! zv6Wf&qT=yZi5b&qPG31|YXjL!P6tmo&py`I){CdvKJNIAy3cjDsqV&8`&Wrni}(@B z+yOpBlA*Q_u&MV`ukDj zAC4$rq!Ke%;fby0eE%K@Z=fVuLRcyA^Iod#6sq)Rupf>_&wZ)US;R;F<{thQl;LaJ zfSppe=;X6&^!q{d02EYOIjy|VocNvz*YdnnOx2mDPte zJKcL-D<+vcxdq;H{>yzTD9T<;d_ODlR~e8`$*+X;LEe}P+@>R<{LaJt^D@^z6*);& zw1NNLK70+mxLc0V{7ju!pwp}dvQUv7$4_WY=tnAx6<|56~0 zoHt@Nle}G=$(Nb(`FXA?&?S^+!&#B!kx$vT4W_T29{QU1d^rESK=NW50{OXVpjzmo z&}O*ilASI3>L<)on}h9i7u`W;xRT-@Fvvd;y}?W05wIFe_Dx|bdepa@%)ue(5WnKi zsRaC9pSPH=gzrOokB>kxP~BI<_tux{J3^%`diyhQ+n)KK`{iAIgOAq?-lc4iJCw`~ z_iae_!NthEIIEqH&K-ZDmTbySxflns|*SdJSF@U**~lVcfosWs`Vc*!8+dh7n+|W zOK!_p?vq2NL#A`^8dalW9FK?8bM-&9f!a`AqpX2(9M3#(u|AC_qXUrVMSAt*NS??| zHe$jB)&Z%vhl7UUM&VN7k|ZUT;gkL-s^u-j9dBk1FLUoFp+{^i57EJl#W}PI%Hm-p zzMl=5!&VsQ^{M8^gikYDDaW%t5HtX>ATJP2_j){wvN6xD2WO-`kiJxU+X}o((sw>Y z2QEI!-RPlL>Q_ZI1()$1m09XC{Zjh1$*^d?jn*vkjEk{m4)fM4{F(#*!vefyMBmv? z--bzaaq5|;bO&d3H$fT@4$I%*mhJ{suum7yzwedq6_bpgqxb4ewza~$*BD&NKYI?` z;m3YZ3BD=8@bq>mFnP}Ft^hN$++y%PJUXxLIS?)TJK&H0_@(X?8HUq!({w$!Sww^V z1q_;I>^@4+gUk6K+)lZl-IRM%y1g5Ei{|N00>T zXd=&J8hXTw?D128Jgd97Ej|Ed-VEWspJn%7%vaoZk9?PT@B-?2>*5?{@IJ(A?icn5 z>7EQvYig|ipbNU?w)ocV^X~V4OwL{p^1}L&FM7^*-dD%}F}uax{^ocVod_Hcln9m# zdYQcl-)jRqR-0y{I!t3sT&pv5t2e%D8Hf& zuE(EsH{GM;iwMV8JVT|Qs|J_#AdW4S;9uPh-3^@qKf^2eGBh+)EL4JOMdi#Z7HZ7f zAX<|p@c4JYbs>x|^mX{_@EbJdEwoRW$n4SN_m$bsP@V#*Ss>)=z9Cyup5KN_BbeK5 zqP*d+@qiOP^<^fvkHH=0*js?iZ@;Bl>A(!P2@rmjoZpLh-dFIhqym}ynP6c$_&vqb zXrwX%^~F1-u39gOc4|A+^1rEN*?|pZb}_*)9_3NL=&Yk6{m(eC0qh17!FVtn$Q^u- zu26PqKGnrJ^f?2E!j{OF0Bg~0LS|+&>hwzVE z$V_l(cnDjx3Q^1r&tY1J6xnKLBDrH%gdgj6>mi+@GY~e-PB_*BQLn& zZ<8D}D_X}7WV+Q4$ldy<_Gh$rI>}CGJol@G(Zq%DdDy2v(~B>UYO}hAx`&oGC9Qy)z=CcJ?}R8CoNBn9a(L9c*Y{yYB4aGi$+hES7rh}LuB(ef2o&3x({ zNCSQf!nj}znv334vv1g+d=%96T~!t0g( zW2L%6oo<+65RL5uyaR`tzcg2c6(jl_c}GgwO4$zB_S?)Ts>cH-5M5_EJPr$i58;p& z1TXEcctcj(Tie^hc#gNrj;j@un?$f0UMk`u>kP|GOL0pv^2qkku_u}bn7%OGpg)(L zcrIgZ<6y%ebljVxd!>4GwZGAS%Nh6~9Dr{)uO8E*ehsjs(!|R}YIfOm%5H;lf%hA>_!}je7!*+&$7Chdw{zQJwV7|8R1F3;2RILf@Ne_T-%-LUs zUx)jkpA+AC(W?ANwIx1UE6Em?JRtFP@#A0EmHpQ#-f5Zbwgt!F=9*ym7c&+`MOTCk zO_H%1=zvy1W_Vck8V?!{8)osfo&~<)J?LO)Z+J(4GXRH@>S|54~Y)D^K_w7?8;wXYvZV1XW@z~mDw21 zsYP@{r@75AI{rRcd6xbt&VX_~I?mgtsv70mKB9MLQ9PlZSI#V)j&xij&!@G%_ zr4yTWC7el(cbI!#b{Ag<#|5vULXz1#G)n(o|2|w`X7VY1!rgL)zI*^Gk1?*YJ$4y{f;8{}ueRrF1=+pl`4rwV^k-H?%Lb2CeRXZNIjj z+OwUm9pfK86CtM_Q3##~b5N=~Rry@Euz6|^Csa5|S>Si{>5@r04Bp0G>ppfFakg&o zuov1E+YX{xI|U@8pogtHH-y!;2pxn_|YctDyJe*`^wHN>2qSR;u$WGX4_=){$ zifU9%Y7F&rK7D?DPwwxFk&BU1Kz4AasB}blBzJ-Are*%Ho{moL-ZNB@lfeME?e*bw zW%x3vMR)lQ`VRXZ;kN7b`Fspr{l)#oa7g)r>Gw~(C3l6U>Tky>^DOXRpm(rO@CIlU zY6Ke(zkTtQ<$Y+)lXHwuwrHdX-Pn}Kd(aLp+ELx_x_ngfUG-hK(GKZjxgn~8CQ4(N z7c-Sq?g82RWLzmI&D4j`OSxmV=(lzV0_3^AG0 z$v^jy9k#qD@5rk<4Tb>GFoe)qj-)548LSaJPBrx0|H5AYh~6pHn@Szk)hoHCOMr0a z8{?x=4O9ekfc#uZZ%J=$Zyj$R@D42UEy4}N@05W+pEh<%tDk3pdC`Hs*H-^?cyA^^;LZs%zc*$^v-*-Lj zfAH)2oFqz;bKdhl&)$3Owf4hK8G@tFiOWa#RTBF1IDG%A2fqa)$(ld4jWe7_Nqb4S zmw0;(oEn-}+n)1G4#8KF-uWt?#}dCEa3JqE=Q!tSe40LfcE4xvvd784&VyI{iu(nE zht7v6tq)M09C2O-KZEzdX~$_8(H;(w+6TZ`AnsP@zZIe?l2*ZjseRvi-x6Q=KPq26 zKWPK%kg@J@?{NoF8K_S*41Fa+W>+SWcO~bt^aAae5gH6%*o5o&4(q240x#cgQc29&;u*##|J68OPH(kcLb;wi(p%Pw9RRpstaQR_7taQkS2!ogp`O z7>?j`W?@|*8?DM*!;bb2WLn+GqQdQc!Duj+8HQuP9OlNtXz+11U(dze+ndf%u)T@B zx!oT=>@KyP_|p~4<`y3}2h_9bS~qRk)Zcq>tIxG{!Q=f5UMi7$Y&6h)7X#XJPSQs-t{c6`Bkfw6--7OV~xOF^$i^Fe(MaAVZ{MsuQPjU2Fmo^)=tz@ z37p_JANKd()Jr}2&Q~qfM@R7c>$PYfF3HS3>8(0>J9@Q7M*-D$XWVDp%4BFME)CLv78ajIO!4%?tF7rQn745ngVQR7YCIQzPjkA&tLgjIGQXn;E}hg=WAkO|$jv8tY<-PsNz7ussX_W2KImizYjW((n^Q@HnR&xeA zS29ajtI|@9}YXC)MPY@HV887jJNHq+>XN z>f>Ga3)f4R_)>Y2p5hJ32M)9gr2xl`Qmr>}_jeCKr8Wak$`|JQRuj_;-7c z0h3OqB|G9A_Bo>uf)gmgnwd*Z6G$bids_) zrP7S{9jJ0;@;-fOH%$;PDs-Y~c0S=%wZ&Z!PpR^){o<7)2Mu zhA*$?0>w~guL7As_p=j^W?j(Tpa)Zt>KGkXYwAADpVAB)ogXFbctfiZ)qDVsou;Ob zD9u(yIPx@n+ZTDBk3bQ#nqLC(1czZ4uG=*A&7hqH`$UUygpqM*O#( zxl5J>La41|%94EIE{Gfbib6pvzdyP1hlmrjs)unAx*sAGG_^p9^a z(@FWgW}fr|>W46D-qhPu=oLsibrp?QHuGaM?O95d?NxoM8vo=JoGLp|<7rmeZF(d& zUk}&O&M7%wChTOJd37hy>+QnLB0gjW_y`qNIlRZ|{LIy;0DFL1_&h68+eoLZJ$oh5 zM!e$R^+%w&9^$byhyFjXOM5{Qm`y(?+BV$Q6*LAqcV!T4QxpgR#tT8eTys@Mfe4`g zUmu8nYX+nXsR9Qa%=dO7&+E?I>~J0(L0jfn>fcrc;6ae-z4`Co^Om^^Je4XLc;aH~%ccBrIujg_8Bc`csH@(I7`5`z(yK%}uEUMU* zWZfI`PjN=D4FyUI@NvN?=3B31rEpdVz4ABd9j*rI(JuiZaG+}ZFJryF4Id}pMmo7~ zGOT(>vWGN$61{vPh#Pr7? zE~-18nCj>TADMjXH|MX+Iam*(og>(yY|e#_MeLqk(9o5_pP0$Yn8qyjEGp8$C`?NF z9I_um+3~*pJJ?XoivOCkq9Zw@sRg)8UhpNT2jo-bh(Z)}Td5Yh(;_M$^S;5yBDWpB z_W|ofMS5)_`6#*{sta$CZxpi2c7$`$eyVkG8DCa4?uTghQ}tL1;7i4+iZ2@n)N|ek zelp$eO@@cfBW`2@X@SMn4hLgEFDnA)Y#q}VJi@NIg&xvIDvH(Iy9>x2`|?Vff{*w- zjn4D8Ac-AyDgBk@;6L!F*VwO~bMiwjd+<53ouB@}L9|x;SPLm=LDY}Y;}?|h2|(H= z-P5X*wQV8JVRv(b6%0O||rT1ZP*g&oC*VN7}Knws--oL=`%pmPs( zX3|h}H&I{!_!vfOCT^Zh;3`l(?axoB2Woq2Qon~#xA*acqYddq&EM3picR=aPn>>NF`dKBg*yv(&~uDp+R7V+FQ_q}5-D_MW)!{1NoaHD7k+K} zkrPl;r59c*d>0K@Q?l~+z;+NqPeye}I?(zY$!ksr;k+toHG{puCikvstAUPejZN_~ z3Z=hhTS<2iga>sis79SRAH4ASpHBqp3~4c*gBF~*`jI2jF&P|YN`o5knnhfdvkWOy zow<%&G^Xi9jUOGof&2=^Nzl%+_H=%W)7Rmm;wd_x$zV7LpqC}>a#^M~Rs~gD{$?*~L@hD^ zOmTUD9N_&xbL_x{exrfqo`NUa;ImYTs>{ho4m-!93XzLDG1|= ziG%t#b^aXsq^p@jWBsDrQwxVS-5ne>anN zU?G+N_nyh%Q`oFHesTZW z*bO={qhdX{i#IPE|Lb8;8(*aKSwDcq&Lz&7c;ht#MR{5qX79YD*Rl)D1fgW-MZPX3 z+aA{GohYLsh%JAT@s0Gcd~7}o$nza^58|l@tI_4U#%>!AG*eZa`yzM~>E5D26g6!G zsyWSEZ3)`*l{{doc~=0<173mVBh|Z~%;1stRW$hl{G>*<2Hg3r%wGRJyYwY`W#Ygy z$kV0aJ48=SJkx)HVw=2XJ;^`51M9(TdUJ#6w9CI24hE6i1e2M#yc}yfX{e;X(9y`VLZ(*7BcT@Q{v*5m@NBkE&mFl2|C?4Jg>Vt?YFQjjn zOQtLR!#q|*2r)!!@Ndqyw&AmWhmm{geqyxaesoKA(x1`HhIQ^Aab7KFA6sU6{y(9- zQSatkFaaKFAQ7mhyN3I=>n_g3gRsQgVg8fYF^4gsvH_2VbjAXWuXS$GJh-0aMOTZi z;e@+Uw5(_;{*tXl!}0!PyKb@L{^crxMr$_EqH4w5wqdOLd@8&gTthMBv#EHpnc=*hiBp2%ki98*^)f0w3!2#$eea*TN{E`nM;vEp5GI+ zLr>Izv*FU1yZgC4)?OXRhjGmGqqpEfnN8l)i>`5fs}>cf_UzYz&iYZ0_$7D0&L+_e zp(3)YA_{!{+pge4pxkK`{?s`5nK&>E^r2$@kjk_hwQM*L*HZ?ytIhJ7{`P$|AgV_- zQ$w+3J@@iCV&E0l^93r_qp&+alYQx4KEj_)@-J0?=MaUSFl$q~Ej=gA$|}#tYIHq9 z!7wydfA`+!P?@Eg98bJb8g8c-R=y?gk#tnzSX%SB0#!_*Re&!#jjM6o)KaV9n|`pC zq3Vp|RcP8qB)eUIoVVl6>r*YN8a;%$0`;kQDqH?s8Hc6e3Gy^)_DW;w^3pI>>fs+} zzdXik5VnD7K-zA_d#y%s0Zmz(_2J5FI2E42_f}@+L0fdvZP_`*6<+jSq>poe9rzqy z{bBS7;+QA1!e#)i%_w+=NEqohC^4G==?DWrapr`GuajnIDUdI-3)$S8;1cH->U{T& zU?G?Yq>+#&@eUri8?Ku!A2PV-XyH;=LrLH|P%hFIB}OaI!rh#XX}ntmtolK3!K*88 zkvArYoJ(4#Wxz$!skt0_PgTR_*#07)sDiR{o|&yJp11;f?B-w@x{xQOqpzF}I-r*o2lu0W9c)-#yYig< z+*e$TInP1ThDa@lj)glN7E`^Onaxek?!y{((6i_@jEyQRNtOw z7nk;2nnuN!ZXlNV@w1pWKGeh|)y&;ccnl@t#M&k^qfNZHV%Guov4#`4o%x@~*B1OPMpE*fSp_8RhR9O@Jc?4&paSn{%6Ts!qN zmFb;;TUB2_&>P4qXv+1jXFjXn(%;*U>-GWJ=9^wIb`{7xOWHHctlZZlVNG-jdb#bQxn{NcI+bZ+fv zG)U{o*)?-Wo`To-{5l`Ukn^o4o@xHn&jwpy8ScV?H6}0ZPj)z(S32Hk0!!kh`xq$B z%2y)|gZ2!~YOMtO*Mj@8GkvgS=&Nhf8%soEy50T@^Rja7F=S9{;B$1=S}v$XObX*X zlRj{yn&Y|?Y^0w!3J0=!%0%=t9n7_5jzL`iep1yN4`*U_y z#l0aw`?2cgrQ}UfuwieJQHh+AX5|n&*gW26>B?>37GAkbvgA|rVHS`#e??6x4Uv37 zi#^{N-TB94$U4_o^{#j_^@3mG&sE;9zFkRjXLY1sgP#$1cpEUJ%gyLdPXS-OK#gmo zd;EwjLZAI%t)*3bLY;aF$S+zRPOcHxyP>B68GSwW#;(-1qfP#=-#>zUe-Y2&RA@8Lp}es*^6>uH;npji;&#`+PI*^*ZeO zHR%1ef;-p!u73Ymp4B8WHT4v?192wGf2Ge{%g;=QLAg)I<_yq%l?bMSaD0pU9pWL~ zAne7uah8xFhFKjmkd|0Dr1nLHy}C zasbQmYn@4-p1}%yjF!*KRjTH#X6_kz0Tox2w|~ah^WfRmfnTZFH*$Y0 z;XYbH&p`b)@l}~XoYj3i^p(h4LaFjw!{LUo)AuGKs%BA*a>{m^$nwBe8NXv6@C8V; zlFao_!wd2heqX)6haivl;wklI4ocOdAjR5-zH+6})XYTZ@-J%u8D(o!k!7rG`kK`P7`VY%{3+IvdVfue%gp&U_}4{6U7M72A*N7fe63A}dtym$>Ay zpbt!2Bleo&RQ|V^baE7=aNfc9a59=JF$mtSr@IGvRSTj-S$b;EP`W&3GT}Y$^(<47 zTw%^~F6SB*XH~H@U8Uf4niE%gqmJnV#sFzmegry4crVBTngOm}g!nw&E1I!;9#*6V zyT*L-N#&VduZ^qlo~bV%!eW+Sj*`xgk#EONCtZ8KG>HS5iMhhQ+;Gi*p#R&?46#f6 zNgVK7^c&s;n(?ZbQ4XjVe-DMnUtk|z@9Fkw#zz%nAHk{%qN?|^-$E^W0Ul{F@3d+Q z>BQTCnt(uJDKAm(FHNj;Ug|NVqsYAq)b~>TdxfvnQ+vd}<>{h)by9WMtQ;~<#mZFH zp0u*Zz#5o9)!8%1a29a~jbsmQKql_tDsVI`cXkGu)nT)DM$^v;1FeBLg?E7B7}k8# zH<15MdXB0WI&*H`z`4({7qc}>%sYakAxWo0z8rXW+6^ zKq?UTp;@tCSD3rOQEPFdD=#Pt`VG!A8zN3nP9on9NymEs$P^JW}Ef#H#7u)gC6Dg1m$3hiG<{<~V5& zl&)1X3tx~4sHb@v{!sBjxz=uU9|zqB%)G5E`VI_PfbDX6ETFIbfq1@FmIFd<*xD$?SMB?Z&!ZL56~nG2S1Pv>+HC9>E=q8*Pg0zB2yQ(k};{~ z%%Kj~JT8Bxg4MOxV@(a<9UsHW8px^`iTfv>**Hg7T^H?|y>Z%p%6^Ua+3w@RJrLlN zPlVbB)bmRr+uH`_fkB`bke5P>;06`eI()9$Hx-9CPSLu^Y7v(-kW8&Q9kLwUAle`0 zcd16LAE;!@j9@6gN?k zoVO)eZxr>o>M?2S&jK3;7G2AxC}J9eHmszN?0s-h#W7)9m`!G!V7UIZ_BBRpp;?ea zL3hqNXu$WDv6roBY|qv)$H=>J!?*} zxTZTmYf<_Nyq@Ep49uNrhJMXb> z#2=?p>x*AjJ)r((b#^PAU96lfiB%$xn)v<^^!j^%ia`CB5`2`FM%<@rk$k$I(uJ1; z4S;$RL%?!&wWNArek*C~biF>ryD7#OtbtQ0*-atpYmBAegtIWS{bzs7BjQ zhb9Fm@6zY1M1%)qTjId&ysF)IaOvn%B>`R zlH$k=)`Vtld`U!!H-EE`>6i=no5iTR7W}hjV(G??VfT|mW-4(f#?-5NA4;gy|h-aun5UPbFbUQuz+9l0y25Op2I2Hh|6!>hhO$`upC gqFP1$8Rf$2eI5Zi&o(q{WONwynI0C8|IdH_2YKz?Jpcdz literal 0 HcmV?d00001 diff --git a/src/main/resources/fonts/README_ZXSpectrum-7.md b/src/main/resources/fonts/README_ZXSpectrum-7.md index 9e3db21..1c47fa6 100644 --- a/src/main/resources/fonts/README_ZXSpectrum-7.md +++ b/src/main/resources/fonts/README_ZXSpectrum-7.md @@ -1,6 +1,7 @@ # ZX Spectrum 7 Font -By Sizenko Alexander, [Style-7](http://www.styleseven.com), December 2012. From http://www.styleseven.com/php/get_product.php?product=ZX-Spectrum-7 +By Sizenko Alexander, [Style-7](http://www.styleseven.com), December 2012. +From http://www.styleseven.com/php/get_product.php?product=ZX-Spectrum-7 Minor metrics adjustments by Anya Helene Bagge, October 2017. @@ -8,31 +9,32 @@ Minor metrics adjustments by Anya Helene Bagge, October 2017. # True Type Font: ZX Spectrum-7 version 1.0 - ## EULA The font ZX Spectrum-7 is freeware. - ## DESCRIPTION -This font has been created to mark the 30th anniversary of the great computer ZX-Spectrum. Font based on native ZX-Spectrum symbols. Latin and Cyrillic code pages are supported. + +This font has been created to mark the 30th anniversary of the great computer ZX-Spectrum. Font based on native +ZX-Spectrum symbols. Latin and Cyrillic code pages are supported. Files in zx_spectrum-7.zip: -* readme.txt this file; -* zx_spectrum-7.ttf regular style; -* zx_spectrum-7_bold.ttf bold style; -* zx_spectrum-7_screen.png preview image. + +* readme.txt this file; +* zx_spectrum-7.ttf regular style; +* zx_spectrum-7_bold.ttf bold style; +* zx_spectrum-7_screen.png preview image. Please visit http://www.styleseven.com/ for download our other products as freeware as shareware. We will welcome any useful suggestions and comments; please send them to ms-7@styleseven.com - ## WHAT'S NEW? + * Version 1.01 (December 27 2012): * Fixed bug for Cyrillic code page. - ## AUTHOR + Sizenko Alexander Style-7 http://www.styleseven.com diff --git a/src/main/resources/fonts/README_lmmono10-regular.md b/src/main/resources/fonts/README_lmmono10-regular.md index 1eb6243..8192f72 100644 --- a/src/main/resources/fonts/README_lmmono10-regular.md +++ b/src/main/resources/fonts/README_lmmono10-regular.md @@ -1,13 +1,13 @@ This package was debianized by Florent Rougon on Tue, 27 Jan 2004 20:12:21 +0100. -The following TeX Live packages were downloaded from - http://www.ctan.org/tex-archive/systems/texlive/tlnet/archive/ +The following TeX Live packages were downloaded from +http://www.ctan.org/tex-archive/systems/texlive/tlnet/archive/ and merged into one orig.tar.gz file: - lm.tar.xz - lm.doc.tar.xz - lm-math.tar.xz - lm-math.doc.tar.xz +lm.tar.xz +lm.doc.tar.xz +lm-math.tar.xz +lm-math.doc.tar.xz Upstream work @@ -15,54 +15,52 @@ Upstream work The upstream README-Latin-Modern.txt file says: - Font: The Latin Modern Family of Fonts - Designer (Computer Modern Family of Fonts): Donald E. Knuth - Author: Bogus\l{}aw Jackowski and Janusz M. Nowacki - Version: 2.003 - Date: 16 IX 2009 - Downloads: http://www.gust.org.pl/projects/e-foundry/latin-modern/ - License: - % Copyright 2003--2009 by B. Jackowski and J.M. Nowacki - % (on behalf of TeX Users Groups). - % This work is released under the GUST Font License - % -- see GUST-FONT-LICENSE.txt. - % This work has the LPPL maintenance status "maintained". - % The Current Maintainer of this work is Bogus\l{}aw Jackowski - % and Janusz M. Nowacki. - % This work consists of the files listed in the MANIFEST.txt file. +Font: The Latin Modern Family of Fonts +Designer (Computer Modern Family of Fonts): Donald E. Knuth +Author: Bogus\l{}aw Jackowski and Janusz M. Nowacki +Version: 2.003 +Date: 16 IX 2009 +Downloads: http://www.gust.org.pl/projects/e-foundry/latin-modern/ +License: +% Copyright 2003--2009 by B. Jackowski and J.M. Nowacki +% (on behalf of TeX Users Groups). +% This work is released under the GUST Font License +% -- see GUST-FONT-LICENSE.txt. +% This work has the LPPL maintenance status "maintained". +% The Current Maintainer of this work is Bogus\l{}aw Jackowski +% and Janusz M. Nowacki. +% This work consists of the files listed in the MANIFEST.txt file. - [...] - - The current LM package contains the most recent version - of the Latin Modern family of fonts in the PostScript Type 1 and - OpenType format. The fonts are based on Donald E. Knuth's Computer Modern - fonts in the PostScript Type 1 format, released into public domain by the - American Mathematical Society (for the history of the outline version of - the CM fonts see, e.g., http://www.math.utah.edu/~beebe/fonts/bluesky.html ). - The project is supported by TeX users groups: CSTUG, DANTE eV, GUST, - GUTenberg, NTG, and TUG. +[...] +The current LM package contains the most recent version +of the Latin Modern family of fonts in the PostScript Type 1 and +OpenType format. The fonts are based on Donald E. Knuth's Computer Modern +fonts in the PostScript Type 1 format, released into public domain by the +American Mathematical Society (for the history of the outline version of +the CM fonts see, e.g., http://www.math.utah.edu/~beebe/fonts/bluesky.html ). +The project is supported by TeX users groups: CSTUG, DANTE eV, GUST, +GUTenberg, NTG, and TUG. The current README-Latin-Modern-Math.txt says License: - % Copyright 2012--2014 for the Latin Modern math extensions by B. Jackowski, - % P. Strzelczyk and P. Pianowski (on behalf of TeX Users Groups). - % - % This work can be freely used and distributed under - % the GUST Font License (GFL -- see GUST-FONT-LICENSE.txt) - % which is actually an instance of the LaTeX Project Public License - % (LPPL -- see http://www.latex-project.org/lppl.txt). - % - % This work has the maintenance status "maintained". The Current Maintainer - % of this work is Bogus\l{}aw Jackowski, Piotr Strzelczyk and Piotr Pianowski. - % - % This work consists of the files listed - % in the MANIFEST-Latin-Modern-Math.txt file. - +% Copyright 2012--2014 for the Latin Modern math extensions by B. Jackowski, +% P. Strzelczyk and P. Pianowski (on behalf of TeX Users Groups). +% +% This work can be freely used and distributed under +% the GUST Font License (GFL -- see GUST-FONT-LICENSE.txt) +% which is actually an instance of the LaTeX Project Public License +% (LPPL -- see http://www.latex-project.org/lppl.txt). +% +% This work has the maintenance status "maintained". The Current Maintainer +% of this work is Bogus\l{}aw Jackowski, Piotr Strzelczyk and Piotr Pianowski. +% +% This work consists of the files listed +% in the MANIFEST-Latin-Modern-Math.txt file. See the appendix B for the GUST Font License. -Please read the appendix A below if you want to examine the licensing terms +Please read the appendix A below if you want to examine the licensing terms for the Computer Modern fonts in Type 1 format on which the Latin Modern fonts are based. @@ -73,47 +71,46 @@ Debian packaging Copyright (c) 2004-2007 Florent Rougon Copyright (c) 2005-2015 Norbert Preining - This program is free software; you can redistribute it and/or modify - it under the terms of the GNU General Public License as published by - the Free Software Foundation; version 2 dated June, 1991. +This program is free software; you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation; version 2 dated June, 1991. - This program is distributed in the hope that it will be useful, but - WITHOUT ANY WARRANTY; without even the implied warranty of - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - General Public License for more details. +This program is distributed in the hope that it will be useful, but +WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +General Public License for more details. - You should have received a copy of the GNU General Public License - along with this program; see the file COPYING. If not, write to the - Free Software Foundation, Inc., 51 Franklin St, Fifth Floor, - Boston, MA 02110-1301 USA. +You should have received a copy of the GNU General Public License +along with this program; see the file COPYING. If not, write to the +Free Software Foundation, Inc., 51 Franklin St, Fifth Floor, +Boston, MA 02110-1301 USA. On Debian systems, the complete text of the GNU General Public License version 2 can be found in `/usr/share/common-licenses/GPL-2'. - Appendix A -- Licensing terms for the Computer Modern fonts in Type 1 format - on which the Latin Modern fonts are based. +on which the Latin Modern fonts are based. ----------------------------------------------------------------------------- Running t1disasm (from the t1utils package) on /usr/share/texmf/fonts/type1/bluesky/cm/*.pfb and /usr/share/texmf/fonts/type1/bluesky/cmextra/*.pfb yields: - Copyright (C) 1997 American Mathematical Society. All Rights Reserved +Copyright (C) 1997 American Mathematical Society. All Rights Reserved - [ Florent Rougon's note: well, for - /usr/share/texmf/fonts/type1/bluesky/cm/cmb{,sy}10.pfb, you will only get - one space between "Society." and "All" (with tetex-extra 2.0.2-5.1). ;-) ] +[ Florent Rougon's note: well, for +/usr/share/texmf/fonts/type1/bluesky/cm/cmb{,sy}10.pfb, you will only get +one space between "Society." and "All" (with tetex-extra 2.0.2-5.1). ;-) ] The precise distribution conditions for these fonts can be found in /usr/share/doc/texmf/fonts/bluesky/README from the tetex-doc package. I will duplicate here the relevant excerpt for your convenience: - The PostScript Type 1 implementation of the Computer Modern fonts produced - by and previously distributed by Blue Sky Research and Y&Y, Inc. are now - freely available for general use. This has been accomplished through the - cooperation of a consortium of scientific publishers with Blue Sky Research - and Y&Y. Members of this consortium include: +The PostScript Type 1 implementation of the Computer Modern fonts produced +by and previously distributed by Blue Sky Research and Y&Y, Inc. are now +freely available for general use. This has been accomplished through the +cooperation of a consortium of scientific publishers with Blue Sky Research +and Y&Y. Members of this consortium include: Elsevier Science IBM Corporation @@ -121,18 +118,18 @@ duplicate here the relevant excerpt for your convenience: Springer-Verlag American Mathematical Society (AMS) - In order to assure the authenticity of these fonts, copyright will be held - by the American Mathematical Society. This is not meant to restrict in any - way the legitimate use of the fonts, such as (but not limited to) electronic - distribution of documents containing these fonts, inclusion of these fonts - into other public domain or commercial font collections or computer - applications, use of the outline data to create derivative fonts and/or - faces, etc. However, the AMS does require that the AMS copyright notice be - removed from any derivative versions of the fonts which have been altered in - any way. In addition, to ensure the fidelity of TeX documents using Computer - Modern fonts, Professor Donald Knuth, creator of the Computer Modern faces, - has requested that any alterations which yield different font metrics be - given a different name. +In order to assure the authenticity of these fonts, copyright will be held +by the American Mathematical Society. This is not meant to restrict in any +way the legitimate use of the fonts, such as (but not limited to) electronic +distribution of documents containing these fonts, inclusion of these fonts +into other public domain or commercial font collections or computer +applications, use of the outline data to create derivative fonts and/or +faces, etc. However, the AMS does require that the AMS copyright notice be +removed from any derivative versions of the fonts which have been altered in +any way. In addition, to ensure the fidelity of TeX documents using Computer +Modern fonts, Professor Donald Knuth, creator of the Computer Modern faces, +has requested that any alterations which yield different font metrics be +given a different name. Appendix B -- GUST Font License @@ -155,16 +152,16 @@ upstream distribution of the Latin Modern fonts. % % Please also observe the following clause: % 1) it is requested, but not legally required, that derived works be -% distributed only after changing the names of the fonts comprising this -% work and given in an accompanying "manifest", and that the -% files comprising the Work, as listed in the manifest, also be given -% new names. Any exceptions to this request are also given in the -% manifest. +% distributed only after changing the names of the fonts comprising this +% work and given in an accompanying "manifest", and that the +% files comprising the Work, as listed in the manifest, also be given +% new names. Any exceptions to this request are also given in the +% manifest. % -% We recommend the manifest be given in a separate file named -% MANIFEST-.txt, where is some unique identification -% of the font family. If a separate "readme" file accompanies the Work, -% we recommend a name of the form README-.txt. +% We recommend the manifest be given in a separate file named +% MANIFEST-.txt, where is some unique identification +% of the font family. If a separate "readme" file accompanies the Work, +% we recommend a name of the form README-.txt. % % The latest version of the LaTeX Project Public License is in % http://www.latex-project.org/lppl.txt and version 1.3c or later diff --git a/src/test/java/AreaRetting.java b/src/test/java/AreaRetting.java index 4af85f0..041230d 100644 --- a/src/test/java/AreaRetting.java +++ b/src/test/java/AreaRetting.java @@ -1,123 +1,126 @@ import inf101.v18.grid.GridDirection; import inf101.v18.grid.IArea; -import inf101.v18.grid.ILocation; -import inf101.v18.util.IGenerator; +import inf101.v18.grid.Location; +import inf101.v18.util.Generator; import inf101.v18.util.generators.AreaGenerator; import inf101.v18.util.generators.LocationGenerator; - -import static org.junit.Assert.*; +import org.junit.Test; import java.util.HashSet; import java.util.List; import java.util.Set; -import org.junit.Test; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertTrue; public class AreaRetting { - private static final int N = 10000; + private static final int N = 10000; - private IGenerator areaGen = new AreaGenerator(); + private Generator areaGen = new AreaGenerator(); - public void allAreaProperties() { + public void allAreaProperties() { - } + } - public void canGoProperty(ILocation l, GridDirection dir) { - int x = l.getX() + dir.getDx(); - int y = l.getY() + dir.getDy(); - assertEquals(l.getArea().contains(x, y), l.canGo(dir)); - try { - assertNotNull(l.go(dir)); - assertTrue(String.format("Expected true: %s.canGo(%s) for %d,%d", l, dir, x, y), l.canGo(dir)); - } catch (IndexOutOfBoundsException e) { - assertFalse(String.format("Expected false: %s.canGo(%s) for %d,%d", l, dir, x, y), l.canGo(dir)); - } - } + public void canGoProperty(Location l, GridDirection dir) { + int x = l.getX() + dir.getDx(); + int y = l.getY() + dir.getDy(); + assertEquals(l.getArea().contains(x, y), l.canGo(dir)); + try { + assertNotNull(l.go(dir)); + assertTrue(String.format("Expected true: %s.canGo(%s) for %d,%d", l, dir, x, y), l.canGo(dir)); + } catch (IndexOutOfBoundsException e) { + assertFalse(String.format("Expected false: %s.canGo(%s) for %d,%d", l, dir, x, y), l.canGo(dir)); + } + } - public void distanceProperty(ILocation l1, ILocation l2) { - assertEquals(l1.gridDistanceTo(l2), l2.gridDistanceTo(l1)); - assertEquals(l1.stepDistanceTo(l2), l2.stepDistanceTo(l1)); - assertTrue(l1.gridDistanceTo(l2) <= l1.stepDistanceTo(l2)); - assertTrue(l1.gridDistanceTo(l2) <= l1.geometricDistanceTo(l2)); - } + public void distanceProperty(Location l1, Location l2) { + assertEquals(l1.gridDistanceTo(l2), l2.gridDistanceTo(l1)); + assertEquals(l1.stepDistanceTo(l2), l2.stepDistanceTo(l1)); + assertTrue(l1.gridDistanceTo(l2) <= l1.stepDistanceTo(l2)); + assertTrue(l1.gridDistanceTo(l2) <= l1.geometricDistanceTo(l2)); + } - public void gridLineProperty(ILocation l1, ILocation l2) { - // System.out.println(l1.toString() + " .. " + l2.toString()); - List line = l1.gridLineTo(l2); - assertEquals(l1.gridDistanceTo(l2), line.size()); - ILocation last = l1; - for (ILocation l : line) { - assertEquals(1, last.gridDistanceTo(l)); - last = l; - } - assertEquals(l2, last); - } + public void gridLineProperty(Location l1, Location l2) { + // System.out.println(l1.toString() + " .. " + l2.toString()); + List line = l1.gridLineTo(l2); + assertEquals(l1.gridDistanceTo(l2), line.size()); + Location last = l1; + for (Location l : line) { + assertEquals(1, last.gridDistanceTo(l)); + last = l; + } + assertEquals(l2, last); + } - @Test - public void gridLineTest() { - for (int i = 0; i < 10; i++) { - IArea area = areaGen.generate(); - IGenerator lGen = new LocationGenerator(area); - for (int j = 0; j < N; j++) { - ILocation l1 = lGen.generate(); - ILocation l2 = lGen.generate(); - distanceProperty(l1, l2); - gridLineProperty(l1, l2); - } - } - } + @Test + public void gridLineTest() { + for (int i = 0; i < 10; i++) { + IArea area = areaGen.generate(); + Generator lGen = new LocationGenerator(area); + for (int j = 0; j < N; j++) { + Location l1 = lGen.generate(); + Location l2 = lGen.generate(); + distanceProperty(l1, l2); + gridLineProperty(l1, l2); + } + } + } - @Test - public void locationsTest() { - for (int i = 0; i < 10; i++) { - IArea area = areaGen.generate(); - for (ILocation l : area) { - neighboursDistProperty(l); - neighboursSymmetryProperty(l); - for (GridDirection d : GridDirection.EIGHT_DIRECTIONS) - canGoProperty(l, d); - } - } - } + @Test + public void locationsTest() { + for (int i = 0; i < 10; i++) { + IArea area = areaGen.generate(); + for (Location l : area) { + neighboursDistProperty(l); + neighboursSymmetryProperty(l); + for (GridDirection d : GridDirection.EIGHT_DIRECTIONS) + canGoProperty(l, d); + } + } + } - public void neighboursDistProperty(ILocation loc) { - for (ILocation l : loc.allNeighbours()) { - assertEquals(1, loc.gridDistanceTo(l)); - } - } + public void neighboursDistProperty(Location loc) { + for (Location l : loc.allNeighbours()) { + assertEquals(1, loc.gridDistanceTo(l)); + } + } - public void neighboursSymmetryProperty(ILocation loc) { - for (ILocation l : loc.allNeighbours()) { - assertTrue("My neighbour should have me as a neighbour", l.allNeighbours().contains(loc)); - } - } + public void neighboursSymmetryProperty(Location loc) { + for (Location l : loc.allNeighbours()) { + assertTrue("My neighbour should have me as a neighbour", l.allNeighbours().contains(loc)); + } + } - @Test - public void uniqueLocations() { - for (int i = 0; i < N / 10; i++) { - IArea area = areaGen.generate(); - uniqueLocationsProperty(area); - } - } + @Test + public void uniqueLocations() { + for (int i = 0; i < N / 10; i++) { + IArea area = areaGen.generate(); + uniqueLocationsProperty(area); + } + } - public void uniqueLocationsProperty(IArea area) { - Set set = new HashSet<>(); - for (ILocation l : area) { - assertTrue("Location should be unique: " + l, set.add(l)); - } - } + public void uniqueLocationsProperty(IArea area) { + Set set = new HashSet<>(); + for (Location l : area) { + assertTrue("Location should be unique: " + l, set.add(l)); + } + } - @Test - public void validLocations() { - for (int i = 0; i < N / 10; i++) { - IArea area = areaGen.generate(); - validLocationsProperty(area); - } - } + @Test + public void validLocations() { + for (int i = 0; i < N / 10; i++) { + IArea area = areaGen.generate(); + validLocationsProperty(area); + } + } - public void validLocationsProperty(IArea area) { - for (ILocation l : area) { - assertTrue("Location should be in area: " + l, area.contains(l)); - assertTrue("Location should be in area: " + l, area.contains(l.getX(), l.getY())); - } - } + public void validLocationsProperty(IArea area) { + for (Location l : area) { + assertTrue("Location should be in area: " + l, area.contains(l)); + assertTrue("Location should be in area: " + l, area.contains(l.getX(), l.getY())); + } + } } diff --git a/src/test/java/GameMapTest.java b/src/test/java/GameMapTest.java index 686ca02..c5eb501 100644 --- a/src/test/java/GameMapTest.java +++ b/src/test/java/GameMapTest.java @@ -1,112 +1,112 @@ -import static org.junit.Assert.*; - -import inf101.v18.rogue101.objects.IItem; +import inf101.v18.grid.Location; +import inf101.v18.rogue101.map.AGameMap; +import inf101.v18.rogue101.object.Item; import org.junit.Test; -import inf101.v18.grid.ILocation; -import inf101.v18.rogue101.map.GameMap; - import java.util.List; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; + public class GameMapTest { - @Test - public void testSortedAdd() { - GameMap gameMap = new GameMap(20, 20); - ILocation location = gameMap.getLocation(10, 10); - for (int i = 0; i < 30; i++) { - gameMap.addRandomItems(location); - } - List items = gameMap.getAll(location); - for (int i = 0; i < items.size() - 1; i++) { - assertTrue(items.get(i).compareTo(items.get(i + 1)) >= 0); - } - } + @Test + public void testSortedAdd() { + AGameMap gameMap = new AGameMap(20, 20); + Location location = gameMap.getLocation(10, 10); + for (int i = 0; i < 30; i++) { + gameMap.addRandomItems(location); + } + List items = gameMap.getAll(location); + for (int i = 0; i < items.size() - 1; i++) { + assertTrue(items.get(i).compareTo(items.get(i + 1)) >= 0); + } + } - @Test - public void testGetNeighbours() { - GameMap gameMap = new GameMap(20, 20); - ILocation location = gameMap.getLocation(10, 10); + @Test + public void testGetNeighbours() { + AGameMap gameMap = new AGameMap(20, 20); + Location location = gameMap.getLocation(10, 10); - List neighbours = gameMap.getNeighbourhood(location, 1); - for (ILocation neighbour : neighbours) { - assertTrue(location.gridDistanceTo(neighbour) <= 1); - } - assertEquals(8, neighbours.size()); + List neighbours = gameMap.getNeighbourhood(location, 1); + for (Location neighbour : neighbours) { + assertTrue(location.gridDistanceTo(neighbour) <= 1); + } + assertEquals(8, neighbours.size()); - neighbours = gameMap.getNeighbourhood(location, 2); - for (ILocation neighbour : neighbours) { - assertTrue(location.gridDistanceTo(neighbour) <= 2); - } - assertEquals(24, neighbours.size()); + neighbours = gameMap.getNeighbourhood(location, 2); + for (Location neighbour : neighbours) { + assertTrue(location.gridDistanceTo(neighbour) <= 2); + } + assertEquals(24, neighbours.size()); - neighbours = gameMap.getNeighbourhood(location, 3); - for (ILocation neighbour : neighbours) { - assertTrue(location.gridDistanceTo(neighbour) <= 3); - } - assertEquals(48, neighbours.size()); - } + neighbours = gameMap.getNeighbourhood(location, 3); + for (Location neighbour : neighbours) { + assertTrue(location.gridDistanceTo(neighbour) <= 3); + } + assertEquals(48, neighbours.size()); + } - @Test - public void testNeighboursOutOfBounds() { - //This is a bit overkill, but it's meant to distinguish between kind of working and always working. + @Test + public void testNeighboursOutOfBounds() { + //This is a bit overkill, but it's meant to distinguish between kind of working and always working. - GameMap gameMap = new GameMap(20, 20); + AGameMap gameMap = new AGameMap(20, 20); - ILocation location = gameMap.getLocation(0, 0); - List neighbours = gameMap.getNeighbourhood(location, 3); - for (ILocation neighbour : neighbours) { - assertTrue(location.gridDistanceTo(neighbour) <= 3); - } - assertEquals(15, neighbours.size()); + Location location = gameMap.getLocation(0, 0); + List neighbours = gameMap.getNeighbourhood(location, 3); + for (Location neighbour : neighbours) { + assertTrue(location.gridDistanceTo(neighbour) <= 3); + } + assertEquals(15, neighbours.size()); - location = gameMap.getLocation(19, 0); - neighbours = gameMap.getNeighbourhood(location, 3); - for (ILocation neighbour : neighbours) { - assertTrue(location.gridDistanceTo(neighbour) <= 3); - } - assertEquals(15, neighbours.size()); + location = gameMap.getLocation(19, 0); + neighbours = gameMap.getNeighbourhood(location, 3); + for (Location neighbour : neighbours) { + assertTrue(location.gridDistanceTo(neighbour) <= 3); + } + assertEquals(15, neighbours.size()); - location = gameMap.getLocation(19, 19); - neighbours = gameMap.getNeighbourhood(location, 3); - for (ILocation neighbour : neighbours) { - assertTrue(location.gridDistanceTo(neighbour) <= 3); - } - assertEquals(15, neighbours.size()); + location = gameMap.getLocation(19, 19); + neighbours = gameMap.getNeighbourhood(location, 3); + for (Location neighbour : neighbours) { + assertTrue(location.gridDistanceTo(neighbour) <= 3); + } + assertEquals(15, neighbours.size()); - location = gameMap.getLocation(0, 19); - neighbours = gameMap.getNeighbourhood(location, 3); - for (ILocation neighbour : neighbours) { - assertTrue(location.gridDistanceTo(neighbour) <= 3); - } - assertEquals(15, neighbours.size()); + location = gameMap.getLocation(0, 19); + neighbours = gameMap.getNeighbourhood(location, 3); + for (Location neighbour : neighbours) { + assertTrue(location.gridDistanceTo(neighbour) <= 3); + } + assertEquals(15, neighbours.size()); - location = gameMap.getLocation(0, 10); - neighbours = gameMap.getNeighbourhood(location, 3); - for (ILocation neighbour : neighbours) { - assertTrue(location.gridDistanceTo(neighbour) <= 3); - } - assertEquals(27, neighbours.size()); + location = gameMap.getLocation(0, 10); + neighbours = gameMap.getNeighbourhood(location, 3); + for (Location neighbour : neighbours) { + assertTrue(location.gridDistanceTo(neighbour) <= 3); + } + assertEquals(27, neighbours.size()); - location = gameMap.getLocation(19, 10); - neighbours = gameMap.getNeighbourhood(location, 3); - for (ILocation neighbour : neighbours) { - assertTrue(location.gridDistanceTo(neighbour) <= 3); - } - assertEquals(27, neighbours.size()); + location = gameMap.getLocation(19, 10); + neighbours = gameMap.getNeighbourhood(location, 3); + for (Location neighbour : neighbours) { + assertTrue(location.gridDistanceTo(neighbour) <= 3); + } + assertEquals(27, neighbours.size()); - location = gameMap.getLocation(10, 0); - neighbours = gameMap.getNeighbourhood(location, 3); - for (ILocation neighbour : neighbours) { - assertTrue(location.gridDistanceTo(neighbour) <= 3); - } - assertEquals(27, neighbours.size()); + location = gameMap.getLocation(10, 0); + neighbours = gameMap.getNeighbourhood(location, 3); + for (Location neighbour : neighbours) { + assertTrue(location.gridDistanceTo(neighbour) <= 3); + } + assertEquals(27, neighbours.size()); - location = gameMap.getLocation(10, 19); - neighbours = gameMap.getNeighbourhood(location, 3); - for (ILocation neighbour : neighbours) { - assertTrue(location.gridDistanceTo(neighbour) <= 3); - } - assertEquals(27, neighbours.size()); - } + location = gameMap.getLocation(10, 19); + neighbours = gameMap.getNeighbourhood(location, 3); + for (Location neighbour : neighbours) { + assertTrue(location.gridDistanceTo(neighbour) <= 3); + } + assertEquals(27, neighbours.size()); + } } diff --git a/src/test/java/GridRetting.java b/src/test/java/GridRetting.java index 99b0717..7934690 100644 --- a/src/test/java/GridRetting.java +++ b/src/test/java/GridRetting.java @@ -1,106 +1,111 @@ import inf101.v18.grid.IGrid; -import inf101.v18.grid.ILocation; -import inf101.v18.util.IGenerator; +import inf101.v18.grid.Location; +import inf101.v18.util.Generator; import inf101.v18.util.generators.GridGenerator; import inf101.v18.util.generators.LocationGenerator; import inf101.v18.util.generators.StringGenerator; - -import static org.junit.Assert.assertEquals; +import org.junit.Test; import java.util.function.Function; -import org.junit.Test; +import static org.junit.Assert.assertEquals; public class GridRetting { - private static final int N = 10000; + private static final int N = 10000; - private IGenerator strGen = new StringGenerator(); - private IGenerator> gridGen = new GridGenerator(strGen); + private Generator strGen = new StringGenerator(); + private Generator> gridGen = new GridGenerator(strGen); - public void fillProperty1(IGrid grid, T val) { - grid.fill(val); - for (ILocation l : grid.locations()) { - assertEquals(val, grid.get(l)); - } - } + public void fillProperty1(IGrid grid, T val) { + grid.fill(val); + for (Location l : grid.locations()) { + assertEquals(val, grid.get(l)); + } + } - public void fillProperty2(IGrid grid, Function fun) { - grid.fill(fun); - for (ILocation l : grid.locations()) { - assertEquals(fun.apply(l), grid.get(l)); - } - } + public void fillProperty2(IGrid grid, Function fun) { + grid.fill(fun); + for (Location l : grid.locations()) { + assertEquals(fun.apply(l), grid.get(l)); + } + } - @Test - public void fillTest1() { - for (int i = 0; i < N / 10; i++) { - IGrid grid = gridGen.generate(); + @Test + public void fillTest1() { + for (int i = 0; i < N / 10; i++) { + IGrid grid = gridGen.generate(); - String s = strGen.generate(); - fillProperty1(grid, s); - } - } + String s = strGen.generate(); + fillProperty1(grid, s); + } + } - @Test - public void fillTest2() { - for (int i = 0; i < N / 10; i++) { - IGrid grid = gridGen.generate(); + @Test + public void fillTest2() { + for (int i = 0; i < N / 10; i++) { + IGrid grid = gridGen.generate(); - fillProperty2(grid, (l) -> l.toString()); - } - } + fillProperty2(grid, (l) -> l.toString()); + } + } - /** A set on (x1,y1) doesn't affect a get on a different (x2,y2) */ - public void setGetIndependentProperty(IGrid grid, ILocation l1, ILocation l2, T val) { - if (!l1.equals(l2)) { - T s = grid.get(l2); - grid.set(l1, val); - assertEquals(s, grid.get(l2)); - } - } + /** + * A set on (x1,y1) doesn't affect a get on a different (x2,y2) + */ + public void setGetIndependentProperty(IGrid grid, Location l1, Location l2, T val) { + if (!l1.equals(l2)) { + T s = grid.get(l2); + grid.set(l1, val); + assertEquals(s, grid.get(l2)); + } + } - @Test - public void setGetIndependentTest() { - for (int j = 0; j < 10; j++) { - IGrid grid = gridGen.generate(); - IGenerator lGen = new LocationGenerator(grid.getArea()); + @Test + public void setGetIndependentTest() { + for (int j = 0; j < 10; j++) { + IGrid grid = gridGen.generate(); + Generator lGen = new LocationGenerator(grid.getArea()); - for (int i = 0; i < N; i++) { - ILocation l1 = lGen.generate(); - ILocation l2 = lGen.generate(); - String s = strGen.generate(); + for (int i = 0; i < N; i++) { + Location l1 = lGen.generate(); + Location l2 = lGen.generate(); + String s = strGen.generate(); - setGetIndependentProperty(grid, l1, l2, s); - } - } - } + setGetIndependentProperty(grid, l1, l2, s); + } + } + } - /** get(x,y) is val after set(x, y, val) */ - public void setGetProperty(IGrid grid, ILocation l, T val) { - grid.set(l, val); - assertEquals(val, grid.get(l)); - } + /** + * get(x,y) is val after set(x, y, val) + */ + public void setGetProperty(IGrid grid, Location l, T val) { + grid.set(l, val); + assertEquals(val, grid.get(l)); + } - /** Test that get gives back the same value after set. */ - @Test - public void setGetTest() { - for (int j = 0; j < 10; j++) { - IGrid grid = gridGen.generate(); - IGenerator lGen = new LocationGenerator(grid.getArea()); + /** + * Test that get gives back the same value after set. + */ + @Test + public void setGetTest() { + for (int j = 0; j < 10; j++) { + IGrid grid = gridGen.generate(); + Generator lGen = new LocationGenerator(grid.getArea()); - for (int i = 0; i < N; i++) { - ILocation l = lGen.generate(); - String s = strGen.generate(); + for (int i = 0; i < N; i++) { + Location l = lGen.generate(); + String s = strGen.generate(); - setGetProperty(grid, l, s); - } - } - } + setGetProperty(grid, l, s); + } + } + } - @Test - public void uniqueLocations() { - for (int i = 0; i < N / 10; i++) { - } - } + @Test + public void uniqueLocations() { + for (int i = 0; i < N / 10; i++) { + } + } } diff --git a/src/test/java/PlayerTest.java b/src/test/java/PlayerTest.java index bb3931e..cb4f53a 100644 --- a/src/test/java/PlayerTest.java +++ b/src/test/java/PlayerTest.java @@ -1,25 +1,24 @@ -import static org.junit.Assert.*; - +import inf101.v18.grid.GridDirection; +import inf101.v18.grid.Location; +import inf101.v18.rogue101.game.RogueGame; +import inf101.v18.rogue101.object.PlayerCharacter; +import javafx.scene.input.KeyCode; import org.junit.Test; -import inf101.v18.grid.GridDirection; -import inf101.v18.grid.ILocation; -import inf101.v18.rogue101.game.Game; -import inf101.v18.rogue101.objects.IPlayer; -import javafx.scene.input.KeyCode; +import static org.junit.Assert.assertEquals; public class PlayerTest { - @Test - public void testOutOfBounds() { + @Test + public void testOutOfBounds() { String NO_WALLS_MAP = "1 1\n" + "@\n"; - Game game = new Game(NO_WALLS_MAP); - IPlayer player = (IPlayer) game.setCurrent(0, 0); - ILocation loc = game.getLocation(); - player.keyPressed(game, KeyCode.LEFT); - assertEquals(loc, game.getLocation()); - } + RogueGame game = new RogueGame(NO_WALLS_MAP); + PlayerCharacter player = (PlayerCharacter) game.setCurrent(0, 0); + Location loc = game.getLocation(); + player.keyPressed(game, KeyCode.LEFT); + assertEquals(loc, game.getLocation()); + } /*@Test public void testActor() { @@ -42,55 +41,55 @@ public class PlayerTest { assertEquals(loc, game.getLocation()); }*/ //TODO: Fix error "Toolkit not initialized" - @Test - public void testItem() { - String CARROT_MAP = "5 5\n" - + "#####\n" - + "#CCC#\n" - + "#C@C#\n" - + "#CCC#\n" - + "#####\n"; - Game game = new Game(CARROT_MAP); - IPlayer player = (IPlayer) game.setCurrent(2, 2); - ILocation loc = game.getLocation(); - player.keyPressed(game, KeyCode.RIGHT); - assertEquals(loc.go(GridDirection.EAST), game.getLocation()); - game.doTurn(); - player.keyPressed(game, KeyCode.LEFT); - game.doTurn(); - player.keyPressed(game, KeyCode.LEFT); - assertEquals(loc.go(GridDirection.WEST), game.getLocation()); - } + @Test + public void testItem() { + String CARROT_MAP = "5 5\n" + + "#####\n" + + "#CCC#\n" + + "#C@C#\n" + + "#CCC#\n" + + "#####\n"; + RogueGame game = new RogueGame(CARROT_MAP); + PlayerCharacter player = (PlayerCharacter) game.setCurrent(2, 2); + Location loc = game.getLocation(); + player.keyPressed(game, KeyCode.RIGHT); + assertEquals(loc.go(GridDirection.EAST), game.getLocation()); + game.doTurn(); + player.keyPressed(game, KeyCode.LEFT); + game.doTurn(); + player.keyPressed(game, KeyCode.LEFT); + assertEquals(loc.go(GridDirection.WEST), game.getLocation()); + } - @Test - public void testWallsAndKeys() { //This probably seems a little overkill, but we need to check all keys. - String EMPTY_MAP = "5 5\n" - + "#####\n" - + "# #\n" - + "# @ #\n" - + "# #\n" - + "#####\n"; - Game game = new Game(EMPTY_MAP); - IPlayer player = (IPlayer) game.setCurrent(2, 2); - ILocation loc = game.getLocation(); - player.keyPressed(game, KeyCode.UP); - game.doTurn(); - player.keyPressed(game, KeyCode.UP); - game.doTurn(); - assertEquals(loc.go(GridDirection.NORTH), game.getLocation()); - player.keyPressed(game, KeyCode.DOWN); - game.doTurn(); - player.keyPressed(game, KeyCode.RIGHT); - game.doTurn(); - assertEquals(loc.go(GridDirection.EAST), game.getLocation()); - player.keyPressed(game, KeyCode.LEFT); - game.doTurn(); - player.keyPressed(game, KeyCode.DOWN); - game.doTurn(); - assertEquals(loc.go(GridDirection.SOUTH), game.getLocation()); - player.keyPressed(game, KeyCode.UP); - game.doTurn(); - player.keyPressed(game, KeyCode.LEFT); - assertEquals(loc.go(GridDirection.WEST), game.getLocation()); - } + @Test + public void testWallsAndKeys() { //This probably seems a little overkill, but we need to check all keys. + String EMPTY_MAP = "5 5\n" + + "#####\n" + + "# #\n" + + "# @ #\n" + + "# #\n" + + "#####\n"; + RogueGame game = new RogueGame(EMPTY_MAP); + PlayerCharacter player = (PlayerCharacter) game.setCurrent(2, 2); + Location loc = game.getLocation(); + player.keyPressed(game, KeyCode.UP); + game.doTurn(); + player.keyPressed(game, KeyCode.UP); + game.doTurn(); + assertEquals(loc.go(GridDirection.NORTH), game.getLocation()); + player.keyPressed(game, KeyCode.DOWN); + game.doTurn(); + player.keyPressed(game, KeyCode.RIGHT); + game.doTurn(); + assertEquals(loc.go(GridDirection.EAST), game.getLocation()); + player.keyPressed(game, KeyCode.LEFT); + game.doTurn(); + player.keyPressed(game, KeyCode.DOWN); + game.doTurn(); + assertEquals(loc.go(GridDirection.SOUTH), game.getLocation()); + player.keyPressed(game, KeyCode.UP); + game.doTurn(); + player.keyPressed(game, KeyCode.LEFT); + assertEquals(loc.go(GridDirection.WEST), game.getLocation()); + } }