From 2d53047e7520912745101fccc11c942caf583249 Mon Sep 17 00:00:00 2001 From: jzitnik-dev Date: Wed, 19 Feb 2025 16:11:36 +0100 Subject: [PATCH] feat: Implemented breaking animation --- src/main/java/cz/jzitnik/game/Block.java | 34 ++- src/main/java/cz/jzitnik/game/Game.java | 210 ++++++++++++------ .../java/cz/jzitnik/game/MouseHandler.java | 34 ++- .../java/cz/jzitnik/game/SpriteLoader.java | 4 +- src/main/java/cz/jzitnik/game/items/Item.java | 22 ++ .../java/cz/jzitnik/game/items/ItemType.java | 6 + .../cz/jzitnik/game/sprites/Breaking.java | 12 +- .../java/cz/jzitnik/tui/ScreenRenderer.java | 11 +- .../java/cz/jzitnik/tui/SpriteCombiner.java | 17 +- src/main/resources/textures/breaking/1.ans | 26 +-- src/main/resources/textures/breaking/2.ans | 26 +++ src/main/resources/textures/breaking/3.ans | 26 +++ 12 files changed, 311 insertions(+), 117 deletions(-) create mode 100644 src/main/java/cz/jzitnik/game/items/Item.java create mode 100644 src/main/java/cz/jzitnik/game/items/ItemType.java create mode 100644 src/main/resources/textures/breaking/2.ans create mode 100644 src/main/resources/textures/breaking/3.ans diff --git a/src/main/java/cz/jzitnik/game/Block.java b/src/main/java/cz/jzitnik/game/Block.java index 7b8bb31..708bd11 100644 --- a/src/main/java/cz/jzitnik/game/Block.java +++ b/src/main/java/cz/jzitnik/game/Block.java @@ -1,18 +1,44 @@ package cz.jzitnik.game; -import lombok.AllArgsConstructor; +import cz.jzitnik.game.items.ItemType; import lombok.Getter; -@AllArgsConstructor +import java.util.Optional; + @Getter public class Block { private String blockId; private SpriteLoader.SPRITES sprite; - private boolean ghost; + private Optional spriteState = Optional.empty(); + private boolean ghost = false; + private int hardness = 1; + private Optional tool = Optional.empty(); public Block(String blockId, SpriteLoader.SPRITES sprite) { this.blockId = blockId; this.sprite = sprite; - this.ghost = false; + } + + public Block(String blockId, SpriteLoader.SPRITES sprite, boolean ghost) { + this.blockId = blockId; + this.sprite = sprite; + this.ghost = ghost; + } + + public Block(String blockId, SpriteLoader.SPRITES sprite, int hardness) { + this.blockId = blockId; + this.sprite = sprite; + this.hardness = hardness; + } + + public Block(String blockId, SpriteLoader.SPRITES sprite, int hardness, ItemType tool) { + this.blockId = blockId; + this.sprite = sprite; + this.hardness = hardness; + this.tool = Optional.of(tool); + } + + public void setSpriteState(Enum spriteState) { + this.spriteState = Optional.of(spriteState); } } diff --git a/src/main/java/cz/jzitnik/game/Game.java b/src/main/java/cz/jzitnik/game/Game.java index 822be41..5b3dced 100644 --- a/src/main/java/cz/jzitnik/game/Game.java +++ b/src/main/java/cz/jzitnik/game/Game.java @@ -1,50 +1,102 @@ package cz.jzitnik.game; +import cz.jzitnik.game.items.Item; +import cz.jzitnik.game.items.ItemType; +import cz.jzitnik.game.sprites.Breaking; +import cz.jzitnik.tui.ScreenMovingCalculationProvider; import cz.jzitnik.tui.ScreenRenderer; import lombok.Getter; +import org.jline.terminal.Terminal; import java.util.ArrayList; import java.util.List; +import java.util.Optional; +import java.util.Random; @Getter public class Game { - private List[][] world = new ArrayList[50][50]; + private List[][] world = new ArrayList[256][512]; private Block player; - private boolean mining; + private boolean mining = false; + + private List inventory; + private Optional itemInHand = Optional.empty(); public Game() { - for (int i = 0; i < 50; i++) { - for (int j = 0; j < 50; j++) { + for (int i = 0; i < 256; i++) { + for (int j = 0; j < 512; j++) { world[i][j] = new ArrayList<>(); } } Block steveBlock = new Block("steve", SpriteLoader.SPRITES.STEVE); - world[9][0].add(new Block("grass", SpriteLoader.SPRITES.GRASS)); player = steveBlock; - for (int i = 0; i < 50; i++) { - world[11][i].add(new Block("grass", SpriteLoader.SPRITES.GRASS)); - world[12][i].add(new Block("dirt", SpriteLoader.SPRITES.DIRT)); - world[13][i].add(new Block("dirt", SpriteLoader.SPRITES.DIRT)); - world[14][i].add(new Block("dirt", SpriteLoader.SPRITES.DIRT)); + Random random = new Random(); + int baseHeight = 120; // Base ground level + int[] terrainHeight = new int[512]; - for (int j = 15; j < 49; j++) { - world[j][i].add(new Block("stone", SpriteLoader.SPRITES.STONE)); - } - - world[49][i].add(new Block("bedrock", SpriteLoader.SPRITES.BEDROCK)); + // Generate terrain with gradual height variations + terrainHeight[0] = baseHeight; + for (int i = 1; i < 512; i++) { + int heightChange = random.nextInt(3) - 1; // -1, 0, or +1 + terrainHeight[i] = Math.max(100, Math.min(140, terrainHeight[i - 1] + heightChange)); } - for (int i = 0; i < world.length; i++) { - for (int j = 0; j < world[i].length; j++) { - if (world[i][j].isEmpty()) { - world[i][j].add(new Block("air", SpriteLoader.SPRITES.AIR, true)); // Fill with air + // Smooth terrain to avoid unnatural peaks and dips + for (int i = 2; i < 510; i++) { + terrainHeight[i] = (terrainHeight[i - 1] + terrainHeight[i] + terrainHeight[i + 1]) / 3; + } + + // Generate world blocks based on terrain height + for (int i = 0; i < 512; i++) { + int hillHeight = terrainHeight[i]; + + world[hillHeight][i].add(new Block("grass", SpriteLoader.SPRITES.GRASS)); + + // Dirt layers + for (int j = 1; j <= 4; j++) { + if (hillHeight + j < 256) { + world[hillHeight + j][i].add(new Block("dirt", SpriteLoader.SPRITES.DIRT)); + world[hillHeight + j][i].add(new Block("dirt", SpriteLoader.SPRITES.DIRT)); + } + } + + // Stone layers below dirt + for (int j = hillHeight + 5; j < 250; j++) { + world[j][i].add(new Block("stone", SpriteLoader.SPRITES.STONE, 3)); + } + + // Bedrock at the bottom + world[255][i].add(new Block("bedrock", SpriteLoader.SPRITES.BEDROCK)); + } + + // Fill empty spaces with air + for (List[] lists : world) { + for (List list : lists) { + if (list.isEmpty()) { + list.add(new Block("air", SpriteLoader.SPRITES.AIR, true)); } } } - world[10][0].add(steveBlock); + // Spawn player at a valid starting point + world[terrainHeight[256] - 1][256].add(steveBlock); + } + + public int calculateHardness(Block block) { + int holdingDecrease = 0; + if (itemInHand.isPresent() && block.getTool().isPresent() && itemInHand.get().getType().equals(block.getTool().get())) { + holdingDecrease = itemInHand.get().getMiningDecrease(); + } + + int decrease = block.getHardness() - holdingDecrease; + + if (decrease < 0) { + decrease = 0; + } + + return decrease; } public int[] getPlayerCords() { @@ -70,26 +122,7 @@ public class Game { world[cords[1]][cords[0] + 1].add(player); world[cords[1]][cords[0]].remove(player); - - new Thread(() -> { - while (true) { - try { - Thread.sleep(200); - } catch (InterruptedException e) { - throw new RuntimeException(e); - } - int[] cords2 = getPlayerCords(); - - if (world[cords2[1] + 1][cords2[0]].stream().anyMatch(Block::isGhost)) { - world[cords2[1] + 1][cords2[0]].add(player); - world[cords2[1]][cords2[0]].remove(player); - - screenRenderer.render(world); - } else { - break; - } - } - }).start(); + update(screenRenderer); } public void movePlayerLeft(ScreenRenderer screenRenderer) { @@ -102,25 +135,7 @@ public class Game { world[cords[1]][cords[0] - 1].add(player); world[cords[1]][cords[0]].remove(player); - new Thread(() -> { - while (true) { - try { - Thread.sleep(200); - } catch (InterruptedException e) { - throw new RuntimeException(e); - } - int[] cords2 = getPlayerCords(); - - if (world[cords2[1] + 1][cords2[0]].stream().anyMatch(Block::isGhost)) { - world[cords2[1] + 1][cords2[0]].add(player); - world[cords2[1]][cords2[0]].remove(player); - - screenRenderer.render(world); - } else { - break; - } - } - }).start(); + update(screenRenderer); } public void movePlayerUp(ScreenRenderer screenRenderer) { @@ -151,6 +166,54 @@ public class Game { } public void mine(ScreenRenderer screenRenderer, int x, int y) { + if (mining) { + return; + } + + Block breakingBlock = new Block("breaking", SpriteLoader.SPRITES.BREAKING); + world[y][x].add(breakingBlock); + screenRenderer.render(world); + + int hardness = calculateHardness(world[y][x].stream().filter(block -> !block.isGhost()).toList().get(0)); + + this.mining = true; + + new Thread(() -> { + try { + Thread.sleep(hardness * 166L); + } catch (InterruptedException e) { + throw new RuntimeException(e); + } + breakingBlock.setSpriteState(Breaking.BreakingState.SECOND); + screenRenderer.render(world); + + try { + Thread.sleep(hardness * 166L); + } catch (InterruptedException e) { + throw new RuntimeException(e); + } + breakingBlock.setSpriteState(Breaking.BreakingState.THIRD); + screenRenderer.render(world); + + try { + Thread.sleep(hardness * 166L); + } catch (InterruptedException e) { + throw new RuntimeException(e); + } + + mining = false; + + world[y][x].clear(); + world[y][x].add(new Block("air", SpriteLoader.SPRITES.AIR, true)); + screenRenderer.render(world); + + update(screenRenderer); + }).start(); + } + + public boolean isMineable(int x, int y, Terminal terminal) { + List blocks = world[y][x]; + int[] cords = getPlayerCords(); int playerX = cords[0]; @@ -159,19 +222,22 @@ public class Game { int distanceX = Math.abs(playerX - x); int distanceY = Math.abs(playerY - y); - if (distanceX > 5 || distanceY > 5) { - return; - } + int[] data = ScreenMovingCalculationProvider.calculate(playerX, playerY, terminal.getHeight(), terminal.getWidth(), world[0].length, world.length); - if (world[y][x].stream().allMatch(Block::isGhost)) { - return; - } + int startX = data[0]; + int endX = data[1]; + int startY = data[2]; + int endY = data[3]; - world[y][x].clear(); - world[y][x].add(new Block("air", SpriteLoader.SPRITES.AIR, true)); - screenRenderer.render(world); + return + y >= startY && y < endY && x >= startX && x < endX && + !blocks.stream().allMatch(block -> block.getBlockId().equals("air")) + && distanceX <= 5 && distanceY <= 5 + && !(playerX == x && playerY == y); + } - new Thread(() -> { + public void update(ScreenRenderer screenRenderer) { + while (true) { try { Thread.sleep(200); } catch (InterruptedException e) { @@ -179,12 +245,14 @@ public class Game { } int[] cords2 = getPlayerCords(); - if (world[cords2[1] + 1][cords2[0]].stream().anyMatch(Block::isGhost)) { + if (world[cords2[1] + 1][cords2[0]].stream().allMatch(Block::isGhost)) { world[cords2[1] + 1][cords2[0]].add(player); world[cords2[1]][cords2[0]].remove(player); screenRenderer.render(world); + } else { + break; } - }).start(); + } } } \ No newline at end of file diff --git a/src/main/java/cz/jzitnik/game/MouseHandler.java b/src/main/java/cz/jzitnik/game/MouseHandler.java index 6b84bae..01e1461 100644 --- a/src/main/java/cz/jzitnik/game/MouseHandler.java +++ b/src/main/java/cz/jzitnik/game/MouseHandler.java @@ -40,28 +40,22 @@ public class MouseHandler { int[] data = ScreenMovingCalculationProvider.calculate(playerX, playerY, terminal.getHeight(), terminal.getWidth(), game.getWorld()[0].length, game.getWorld().length); int startX = data[0]; - int endX = data[1]; int startY = data[2]; - int endY = data[3]; int blockX = startX + (mouseX / 50); // 50 chars wide per sprite int blockY = startY + (mouseY / 25); // 25 lines high per sprite - if (blockY == playerX && blockY == playerY) { - return; - } - - if (blockY >= startY && blockY < endY && blockX >= startX && blockX < endX) { - List blocks = game.getWorld()[blockY][blockX]; - System.out.println(blockX); - System.out.println(blockY); - if (!blocks.isEmpty()) { - game.mine(screenRenderer, blockX, blockY); - } + if (game.isMineable(blockX, blockY, terminal)) { + screenRenderer.setSelectedBlock(Optional.empty()); + game.mine(screenRenderer, blockX, blockY); } } private void move(MouseEvent mouseEvent) { + if (game.isMining()) { + return; + } + int mouseX = mouseEvent.getX(); int mouseY = mouseEvent.getY(); @@ -74,9 +68,7 @@ public class MouseHandler { int[] data = ScreenMovingCalculationProvider.calculate(playerX, playerY, terminal.getHeight(), terminal.getWidth(), game.getWorld()[0].length, game.getWorld().length); int startX = data[0]; - int endX = data[1]; int startY = data[2]; - int endY = data[3]; int blockX = startX + (mouseX / 50); // 50 chars wide per sprite int blockY = startY + (mouseY / 25); // 25 lines high per sprite @@ -85,14 +77,20 @@ public class MouseHandler { return; } - List blocks = game.getWorld()[blockY][blockX]; - - if (blockY >= startY && blockY < endY && blockX >= startX && blockX < endX && !blocks.stream().allMatch(block -> block.getBlockId().equals("air"))) { + if (game.isMineable(blockX, blockY, terminal)) { List list = new ArrayList<>(); list.add(blockX); list.add(blockY); + + if (screenRenderer.getSelectedBlock().isPresent() && screenRenderer.getSelectedBlock().get().get(0).equals(blockX) && screenRenderer.getSelectedBlock().get().get(1).equals(blockY)) { + return; + } + screenRenderer.setSelectedBlock(Optional.of(list)); } else { + if (screenRenderer.getSelectedBlock().isEmpty()) { + return; + } screenRenderer.setSelectedBlock(Optional.empty()); } diff --git a/src/main/java/cz/jzitnik/game/SpriteLoader.java b/src/main/java/cz/jzitnik/game/SpriteLoader.java index 0571222..e9c17dd 100644 --- a/src/main/java/cz/jzitnik/game/SpriteLoader.java +++ b/src/main/java/cz/jzitnik/game/SpriteLoader.java @@ -13,7 +13,8 @@ public class SpriteLoader { GRASS, STEVE, STONE, - BEDROCK + BEDROCK, + BREAKING } public static final HashMap SPRITES_MAP = new HashMap<>(); @@ -25,6 +26,7 @@ public class SpriteLoader { SPRITES_MAP.put(SPRITES.STONE, new Stone()); SPRITES_MAP.put(SPRITES.STEVE, new Steve()); SPRITES_MAP.put(SPRITES.BEDROCK, new Bedrock()); + SPRITES_MAP.put(SPRITES.BREAKING, new Breaking()); } public static SpriteList load() { diff --git a/src/main/java/cz/jzitnik/game/items/Item.java b/src/main/java/cz/jzitnik/game/items/Item.java new file mode 100644 index 0000000..6f0ab05 --- /dev/null +++ b/src/main/java/cz/jzitnik/game/items/Item.java @@ -0,0 +1,22 @@ +package cz.jzitnik.game.items; + +import cz.jzitnik.tui.Sprite; +import lombok.AllArgsConstructor; +import lombok.Getter; + +@Getter +@AllArgsConstructor +public class Item { + private String name; + private ItemType type; + private Sprite sprite; + private int durability; + private int miningDecrease = 0; + + public Item(String name, ItemType type, Sprite sprite, int durability) { + this.name = name; + this.type = type; + this.sprite = sprite; + this.durability = durability; + } +} diff --git a/src/main/java/cz/jzitnik/game/items/ItemType.java b/src/main/java/cz/jzitnik/game/items/ItemType.java new file mode 100644 index 0000000..69e62e5 --- /dev/null +++ b/src/main/java/cz/jzitnik/game/items/ItemType.java @@ -0,0 +1,6 @@ +package cz.jzitnik.game.items; + +public enum ItemType { + PICKAXE, + SHOVEL +} diff --git a/src/main/java/cz/jzitnik/game/sprites/Breaking.java b/src/main/java/cz/jzitnik/game/sprites/Breaking.java index 4231a50..073c0ad 100644 --- a/src/main/java/cz/jzitnik/game/sprites/Breaking.java +++ b/src/main/java/cz/jzitnik/game/sprites/Breaking.java @@ -4,22 +4,26 @@ import cz.jzitnik.tui.ResourceLoader; import cz.jzitnik.tui.Sprite; public class Breaking extends Sprite { - enum BreakingState { + public enum BreakingState { FIRST, SECOND, THIRD } + private String fix(String x) { + return x.replaceAll("\033\\[38;5;1;48;5;16m", "\033[0m"); + } + public String getSprite() { - return ResourceLoader.loadResource("breaking/1.ans"); + return fix(ResourceLoader.loadResource("breaking/1.ans")); } public String getSprite(Enum key) { - return ResourceLoader.loadResource(switch (key) { + return fix(ResourceLoader.loadResource(switch (key) { case BreakingState.FIRST -> "breaking/1.ans"; case BreakingState.SECOND -> "breaking/2.ans"; case BreakingState.THIRD -> "breaking/3.ans"; default -> throw new IllegalStateException("Unexpected value: " + key); - }); + })); } } diff --git a/src/main/java/cz/jzitnik/tui/ScreenRenderer.java b/src/main/java/cz/jzitnik/tui/ScreenRenderer.java index 2b11fd9..1379648 100644 --- a/src/main/java/cz/jzitnik/tui/ScreenRenderer.java +++ b/src/main/java/cz/jzitnik/tui/ScreenRenderer.java @@ -77,7 +77,12 @@ public class ScreenRenderer { for (int x = startX; x < endX; x++) { List blocks = world[y][x]; List sprites = new ArrayList<>(blocks.stream() - .map(block -> spriteList.getSprite(block.getSprite()).getSprite()).toList()); + .map(block -> + block.getSpriteState().isEmpty() ? + spriteList.getSprite(block.getSprite()).getSprite() : + spriteList.getSprite(block.getSprite()).getSprite(block.getSpriteState().get()) + ).toList() + ); if (selectedBlock.isPresent() && selectedBlock.get().get(0) == x && selectedBlock.get().get(1) == y) { StringBuilder stringBuilder = new StringBuilder(); @@ -89,10 +94,12 @@ public class ScreenRenderer { for (int i = 0; i < 23; i++) { stringBuilder.append("\033[38;5;231;48;5;231m▓"); - for (int j = 0; j < 48; j++) { + stringBuilder.append("\033[38;5;231;48;5;231m▓"); + for (int j = 0; j < 46; j++) { stringBuilder.append("\033[0m "); } stringBuilder.append("\033[38;5;231;48;5;231m▓"); + stringBuilder.append("\033[38;5;231;48;5;231m▓"); stringBuilder.append("\n"); } diff --git a/src/main/java/cz/jzitnik/tui/SpriteCombiner.java b/src/main/java/cz/jzitnik/tui/SpriteCombiner.java index 68ff754..cd240f6 100644 --- a/src/main/java/cz/jzitnik/tui/SpriteCombiner.java +++ b/src/main/java/cz/jzitnik/tui/SpriteCombiner.java @@ -22,7 +22,9 @@ public class SpriteCombiner { StringBuilder combinedSprite = new StringBuilder(); - for (int i = 0; i < 25; i++) { + int numRows = Math.min(rows1.length, rows2.length); + + for (int i = 0; i < numRows; i++) { String row1 = rows1[i]; String row2 = rows2[i]; StringBuilder combinedRow = new StringBuilder(); @@ -30,11 +32,17 @@ public class SpriteCombiner { int cursor1 = 0; int cursor2 = 0; - while (cursor2 < row2.length()) { + // Ensure we stay within the bounds of both rows. + while (cursor1 < row1.length() && cursor2 < row2.length()) { String color1 = extractColorCode(row1, cursor1); - char pixel1 = row1.charAt(cursor1 + color1.length()); + int pixelIndex1 = cursor1 + color1.length(); + if (pixelIndex1 >= row1.length()) break; // Avoid out-of-bounds + char pixel1 = row1.charAt(pixelIndex1); + String color2 = extractColorCode(row2, cursor2); - char pixel2 = row2.charAt(cursor2 + color2.length()); + int pixelIndex2 = cursor2 + color2.length(); + if (pixelIndex2 >= row2.length()) break; // Avoid out-of-bounds + char pixel2 = row2.charAt(pixelIndex2); if (color2.equals("\033[0m") && pixel2 == ' ') { combinedRow.append(color1).append(pixel1); @@ -51,6 +59,7 @@ public class SpriteCombiner { return combinedSprite.toString(); } + private static String extractColorCode(String row, int index) { StringBuilder colorCode = new StringBuilder(); diff --git a/src/main/resources/textures/breaking/1.ans b/src/main/resources/textures/breaking/1.ans index b0e6f07..46e544b 100644 --- a/src/main/resources/textures/breaking/1.ans +++ b/src/main/resources/textures/breaking/1.ans @@ -4,19 +4,19 @@                                                                                                                                                          -                                                   -                             ▓▓▓                   -                             ▓▓▓                   -                             ▓▓▓                   -             ▓▓▒             ▓▓▓▓▓▓▓▓▓             -         ░░░ ▓▓░             ▓▓▓▓▓▓▓▓▓             -         ▒▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓             -         ▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓                   -         ▒▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓                   -                   ▓▓▓                             -                   ▓▓▓                             -                   ▓▓▓                             -                   ▓▓▓                             +                             ▓▓▓                   +                            ░▓▓▓                   +                            ░▓▓▓                   +                            ░▓▓▓                   +             ▓▓▓            ░▓▓▓▓▓▓▓▓▓             +         ░▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▒▒▒▒▒░             +         ▒▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓░▓▓░░░             +          ░░░▓▓▓▓▓░▓▓▓░▓▓▓▓▓▓▓▓▓                   +                   ▓▓▓                             +                   ▓▓▓                             +                   ▓▓▓                             +                   ▓▓▓                             +                   ░░░                                                                                                                                                                                      diff --git a/src/main/resources/textures/breaking/2.ans b/src/main/resources/textures/breaking/2.ans new file mode 100644 index 0000000..7b4e75b --- /dev/null +++ b/src/main/resources/textures/breaking/2.ans @@ -0,0 +1,26 @@ +                                                   +                                                   +                            ▓▓▓▓                   +             ░░░            ▓▓▓▓▓░▓                +             ▓▓▓            ▓▓▓▓▓▓▓                +             ▓▓▓            ▓▓▓▓▓▓▓                +             ▓▓▓            ▓▓▓▓▓▓▓                +             ▓▓▓▓▓▓         ▓▓▓▓                   +             ▓▓▓▓▓▓   ▓▓▓▒  ▓▓▓▓                   +             ▓▓▓▓▓▓   ▓▓▓▒  ▓▓▓▓                   +   ▓▓▓▒      ▓▓▓      ▓▓▓▒  ▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓       +   ▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓       +   ▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓       +   ▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓      ▓▓▓          +   ▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓      ▓▓▓          +                   ▓▓▓                ▓▓▓          +                   ▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓                +                   ▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓                +                   ▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓                +                   ▓▓▓                             +                   ▓▓▓                             +                ▓▓▓▓▓▓                             +                ▓▓▓▓▓▓                             +                ▓▓▓▓▓▓                             +                                                   + \ No newline at end of file diff --git a/src/main/resources/textures/breaking/3.ans b/src/main/resources/textures/breaking/3.ans new file mode 100644 index 0000000..bd6db11 --- /dev/null +++ b/src/main/resources/textures/breaking/3.ans @@ -0,0 +1,26 @@ +                         ▓▓▓▓                      +                         ▓▓▓▓                      +      ▓▓▓▓         ▓▓▓▓▓▓▓▓▓▓▓▓▓         ▓▓▓▓      +   ▓▓▓▓▓▓▓▓▓▓▓▓▓   ▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓      +   ▓▓▓▓▓▓▓▓▓▓▓▓▓   ▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓      +   ▓▓▓▓▓▓▓▓▓▓▓▓▓             ▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓░      +   ▓▓▓▓▓▓▓▓▓▓▓▓▓             ▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓░      +   ▓▓▓▓      ▓▓▓▓▓▓▓▓▓       ▓▓▓   ▓▓▓             +▓▓▓▓▓▓▓      ▓▓▓▓▓▓▓▓▓▓▓▓    ▓▓▓   ▓▓▓             +▓▓▓▓▓▓▓      ▓▓▓▓▓▓▓▓▓▓▓▓    ▓▓▓   ▓▓▓             +▓▓▓▓▓▓▓      ▓▓▓   ▓▓▓▓▓▓    ▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓   +   ▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓   +   ▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓   +   ▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓      ▓▓▓▓▓▓▓▓▓▓   +   ▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓      ▓▓▓▓▓▓▓▓▓▓   +   ▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓       ▓▓▓      ▓▓▓▓▓▓▓▓▓▓   +   ▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓   ▓▓▓   ▓▓▓▓   +   ▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓   ▓▓▓   ▓▓▓▓   +                   ▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓         ▓▓▓▓   +         ▓▓▓▓      ▓▓▓   ▓▓▓▓▓▓▓▓▓▓         ░▓▓▓   +         ▓▓▓▓      ▓▓▓   ▓▓▓▓▓▓▓▓▓▓         ░▓▓▓   +      ▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓   ▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓          +      ▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓          ▓▓▓▓▓▓▓▓▓          +      ▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓          ▓▓▓▓▓▓▓▓▓          +                   ▓▓▓                             + \ No newline at end of file