From c6b4af7f720783ebf157f86d5d674679a74424af Mon Sep 17 00:00:00 2001 From: jzitnik-dev Date: Thu, 20 Feb 2025 21:18:43 +0100 Subject: [PATCH] feat: Implemented inventory --- README.md | 3 + src/main/java/cz/jzitnik/Main.java | 31 ++- src/main/java/cz/jzitnik/game/Block.java | 24 +- src/main/java/cz/jzitnik/game/Game.java | 60 ++-- src/main/java/cz/jzitnik/game/Generation.java | 15 +- src/main/java/cz/jzitnik/game/Inventory.java | 263 ++++++++++++++++-- .../java/cz/jzitnik/game/MouseHandler.java | 12 +- .../java/cz/jzitnik/game/SpriteLoader.java | 9 +- src/main/java/cz/jzitnik/game/items/Item.java | 14 +- .../java/cz/jzitnik/game/sprites/Window.java | 6 + .../jzitnik/game/sprites/items/DirtItem.java | 14 - .../game/sprites/items/SimpleItem.java | 20 ++ .../java/cz/jzitnik/game/sprites/ui/Font.java | 27 ++ .../jzitnik/game/sprites/{ => ui}/Number.java | 2 +- .../java/cz/jzitnik/tui/ScreenRenderer.java | 149 +++++----- src/main/resources/textures/items/oak_log.ans | 26 ++ src/main/resources/textures/ui/font.ans | 48 ++++ 17 files changed, 572 insertions(+), 151 deletions(-) create mode 100644 README.md create mode 100644 src/main/java/cz/jzitnik/game/sprites/Window.java delete mode 100644 src/main/java/cz/jzitnik/game/sprites/items/DirtItem.java create mode 100644 src/main/java/cz/jzitnik/game/sprites/items/SimpleItem.java create mode 100644 src/main/java/cz/jzitnik/game/sprites/ui/Font.java rename src/main/java/cz/jzitnik/game/sprites/{ => ui}/Number.java (98%) create mode 100644 src/main/resources/textures/items/oak_log.ans create mode 100644 src/main/resources/textures/ui/font.ans diff --git a/README.md b/README.md new file mode 100644 index 0000000..417a051 --- /dev/null +++ b/README.md @@ -0,0 +1,3 @@ +# 2DCraft + +2D craft is simple game written in Java running in terminal using ANSI art. diff --git a/src/main/java/cz/jzitnik/Main.java b/src/main/java/cz/jzitnik/Main.java index 4de85d8..9bbc5ea 100644 --- a/src/main/java/cz/jzitnik/Main.java +++ b/src/main/java/cz/jzitnik/Main.java @@ -3,6 +3,7 @@ package cz.jzitnik; import cz.jzitnik.game.Game; import cz.jzitnik.game.MouseHandler; import cz.jzitnik.game.SpriteLoader; +import cz.jzitnik.game.sprites.Window; import cz.jzitnik.tui.ScreenRenderer; import org.jline.terminal.MouseEvent; import org.jline.terminal.Terminal; @@ -42,12 +43,26 @@ public class Main { key = terminal.reader().read(); if (key == 'M') { MouseEvent mouseEvent = terminal.readMouseEvent(); - mouseHandler.handle(mouseEvent); + switch (game.getWindow()) { + case WORLD -> mouseHandler.handle(mouseEvent); + case INVENTORY -> game.getInventory().click(mouseEvent, terminal, screenRenderer, game); + } } } } switch (key) { + case '1': + case '2': + case '3': + case '4': + case '5': + case '6': + case '7': + case '8': + case '9': + game.changeSlot(key - 49, screenRenderer); + break; case 'a': game.movePlayerLeft(screenRenderer); screenRenderer.render(game); @@ -60,6 +75,15 @@ public class Main { game.movePlayerUp(screenRenderer); screenRenderer.render(game); break; + case 'e': + if (game.getWindow() != Window.WORLD) { + game.setWindow(Window.WORLD); + game.getInventory().setSelectedItemInv(-1); + } else { + game.setWindow(Window.INVENTORY); + } + screenRenderer.render(game); + break; case 'q': System.out.println("Exiting game..."); isRunning[0] = false; @@ -77,7 +101,10 @@ public class Main { // Game loop (rendering the game) while (isRunning[0]) { - screenRenderer.render(game); + if (game.getWindow() == Window.WORLD) { + // Rerender the game only when default window + screenRenderer.render(game); + } try { Thread.sleep(1000); } catch (InterruptedException e) { diff --git a/src/main/java/cz/jzitnik/game/Block.java b/src/main/java/cz/jzitnik/game/Block.java index a1bccf5..dbf2bea 100644 --- a/src/main/java/cz/jzitnik/game/Block.java +++ b/src/main/java/cz/jzitnik/game/Block.java @@ -43,16 +43,22 @@ public class Block { this.toolVariants = toolVariants; } - public Block(String blockId, SpriteLoader.SPRITES sprite, int hardness, ItemType tool, List toolVariants, List drops) { - this.blockId = blockId; - this.sprite = sprite; - this.hardness = hardness; - this.tool = Optional.of(tool); - this.toolVariants = toolVariants; - this.drops = drops; - } - public void setSpriteState(Enum spriteState) { this.spriteState = Optional.of(spriteState); } + + public int calculateHardness(Inventory inventory) { + int holdingDecrease = 0; + if (inventory.getItemInHand().isPresent() && tool.isPresent() && inventory.getItemInHand().get().getType().equals(tool.get())) { + holdingDecrease = inventory.getItemInHand().get().getMiningDecrease(); + } + + int decrease = hardness - holdingDecrease; + + if (decrease < 0) { + decrease = 0; + } + + return decrease; + } } diff --git a/src/main/java/cz/jzitnik/game/Game.java b/src/main/java/cz/jzitnik/game/Game.java index a0d31e3..a415598 100644 --- a/src/main/java/cz/jzitnik/game/Game.java +++ b/src/main/java/cz/jzitnik/game/Game.java @@ -3,6 +3,7 @@ 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.game.sprites.Window; import cz.jzitnik.tui.ScreenMovingCalculationProvider; import cz.jzitnik.tui.ScreenRenderer; import lombok.Getter; @@ -18,6 +19,8 @@ public class Game { @Setter private Block player; private boolean mining = false; + @Setter + private Window window = Window.WORLD; private Inventory inventory = new Inventory(); @@ -25,21 +28,6 @@ public class Game { Generation.generateWorld(this); } - public int calculateHardness(Block block) { - int holdingDecrease = 0; - if (inventory.getItemInHand().isPresent() && block.getTool().isPresent() && inventory.getItemInHand().get().getType().equals(block.getTool().get())) { - holdingDecrease = inventory.getItemInHand().get().getMiningDecrease(); - } - - int decrease = block.getHardness() - holdingDecrease; - - if (decrease < 0) { - decrease = 0; - } - - return decrease; - } - public int[] getPlayerCords() { for (int i = 0; i < world.length; i++) { for (int j = 0; j < world[i].length; j++) { @@ -54,6 +42,9 @@ public class Game { } public void movePlayerRight(ScreenRenderer screenRenderer) { + if (window != Window.WORLD) { + return; + } int[] cords = getPlayerCords(); if (world[cords[1]][cords[0] + 1].stream().anyMatch(block -> !block.isGhost())) { @@ -62,11 +53,15 @@ public class Game { world[cords[1]][cords[0] + 1].add(player); world[cords[1]][cords[0]].remove(player); + screenRenderer.render(this); update(screenRenderer); } public void movePlayerLeft(ScreenRenderer screenRenderer) { + if (window != Window.WORLD) { + return; + } int[] cords = getPlayerCords(); if (world[cords[1]][cords[0] - 1].stream().anyMatch(block -> !block.isGhost())) { @@ -75,11 +70,15 @@ public class Game { world[cords[1]][cords[0] - 1].add(player); world[cords[1]][cords[0]].remove(player); + screenRenderer.render(this); update(screenRenderer); } public void movePlayerUp(ScreenRenderer screenRenderer) { + if (window != Window.WORLD) { + return; + } int[] cords = getPlayerCords(); if (world[cords[1] - 1][cords[0]].stream().anyMatch(block -> !block.isGhost()) || world[cords[1] + 1][cords[0]].stream().anyMatch(Block::isGhost)) { @@ -107,7 +106,7 @@ public class Game { } public void mine(ScreenRenderer screenRenderer, int x, int y) { - if (mining) { + if (mining || window != Window.WORLD) { return; } @@ -115,7 +114,7 @@ public class Game { world[y][x].add(breakingBlock); screenRenderer.render(this); - int hardness = calculateHardness(world[y][x].stream().filter(block -> !block.isGhost()).toList().get(0)); + int hardness = world[y][x].stream().filter(block -> !block.isGhost()).toList().get(0).calculateHardness(inventory); this.mining = true; @@ -200,7 +199,7 @@ public class Game { public void update(ScreenRenderer screenRenderer) { while (true) { try { - Thread.sleep(200); + Thread.sleep(100); } catch (InterruptedException e) { throw new RuntimeException(e); } @@ -218,8 +217,23 @@ public class Game { } public void build(int x, int y, ScreenRenderer screenRenderer) { + if (window != Window.WORLD) { + return; + } var blocks = world[y][x]; + int[] cords = getPlayerCords(); + + int playerX = cords[0]; + int playerY = cords[1]; + + int distanceX = Math.abs(playerX - x); + int distanceY = Math.abs(playerY - y); + + if (distanceX > 5 || distanceY > 5) { + return; + } + if (!blocks.stream().allMatch(Block::isGhost)) { return; } @@ -231,6 +245,16 @@ public class Game { blocks.removeAll(blocks.stream().filter(block -> block.getBlockId().equals("air")).toList()); blocks.add(inventory.getItemInHand().get().getBlock().get()); + inventory.decreaseItemInHand(); + + screenRenderer.render(this); + } + + public void changeSlot(int slot, ScreenRenderer screenRenderer) { + if (window != Window.WORLD) { + return; + } + inventory.setItemInhHandIndex(slot); screenRenderer.render(this); } } \ No newline at end of file diff --git a/src/main/java/cz/jzitnik/game/Generation.java b/src/main/java/cz/jzitnik/game/Generation.java index 7099f29..ad62045 100644 --- a/src/main/java/cz/jzitnik/game/Generation.java +++ b/src/main/java/cz/jzitnik/game/Generation.java @@ -10,19 +10,24 @@ import java.util.List; import java.util.Random; public class Generation { - private static Block dirt() { + public static Block dirt() { var block = new Block("dirt", SpriteLoader.SPRITES.DIRT, 1, ItemType.SHOVEL, new ArrayList<>()); - block.setDrops(List.of(new Item("Dirt block", ItemType.BLOCK, SpriteLoader.SPRITES.ITEM_DIRT, block))); + block.setDrops(List.of(new Item("dirt", "Dirt block", ItemType.BLOCK, SpriteLoader.SPRITES.ITEM_DIRT, block))); return block; } private static Block grass() { var block = new Block("grass", SpriteLoader.SPRITES.GRASS, 1, ItemType.SHOVEL, new ArrayList<>()); - block.setDrops(List.of(new Item("Dirt block", ItemType.BLOCK, SpriteLoader.SPRITES.ITEM_DIRT, block))); + block.setDrops(List.of(new Item("dirt", "Dirt block", ItemType.BLOCK, SpriteLoader.SPRITES.ITEM_DIRT, dirt()))); return block; } private static Block stone() { var block = new Block("stone", SpriteLoader.SPRITES.STONE, 15, ItemType.PICKAXE, Arrays.stream(ToolVariant.values()).toList()); - block.setDrops(List.of(new Item("Stone block", ItemType.BLOCK, SpriteLoader.SPRITES.ITEM_DIRT, block))); + block.setDrops(List.of(new Item("stone", "Stone block", ItemType.BLOCK, SpriteLoader.SPRITES.ITEM_DIRT, block))); + return block; + } + private static Block oakLog() { + var block = new Block("oak_log", SpriteLoader.SPRITES.OAK_LOG, 3, ItemType.AXE, new ArrayList<>()); + block.setDrops(List.of(new Item("oak_log", "Oak log", ItemType.BLOCK, SpriteLoader.SPRITES.ITEM_OAK_LOG, block))); return block; } @@ -104,7 +109,7 @@ public class Generation { for (int j = 0; j < 3; j++) { if (treeBase - j >= 0) { - world[treeBase - j - 1][i].add(new Block("oak_log", SpriteLoader.SPRITES.OAK_LOG, 2, ItemType.AXE, List.of(ToolVariant.IRON))); + world[treeBase - j - 1][i].add(oakLog()); } } diff --git a/src/main/java/cz/jzitnik/game/Inventory.java b/src/main/java/cz/jzitnik/game/Inventory.java index a0859c4..9d7eda5 100644 --- a/src/main/java/cz/jzitnik/game/Inventory.java +++ b/src/main/java/cz/jzitnik/game/Inventory.java @@ -2,10 +2,13 @@ package cz.jzitnik.game; import cz.jzitnik.game.items.InventoryItem; import cz.jzitnik.game.items.Item; +import cz.jzitnik.tui.ScreenRenderer; import cz.jzitnik.tui.SpriteCombiner; -import cz.jzitnik.game.sprites.Number; +import cz.jzitnik.game.sprites.ui.Number; import cz.jzitnik.tui.SpriteList; import lombok.Getter; +import lombok.Setter; +import org.jline.terminal.MouseEvent; import org.jline.terminal.Terminal; import java.util.ArrayList; @@ -15,29 +18,83 @@ import java.util.Optional; @Getter public class Inventory { private static final int INVENTORY_SIZE_PX = 470; + private static int COLUMN_AMOUNT = 5; + private static int ROW_AMOUNT = 4; - private List items = new ArrayList<>(); + private InventoryItem[] items = new InventoryItem[20]; + private InventoryItem[] hotbar = new InventoryItem[9]; + + @Setter private int itemInhHandIndex = 0; + @Setter + private int selectedItemInv = -1; public Optional getItemInHand() { + if (hotbar[itemInhHandIndex] == null) { + return Optional.empty(); + } + return Optional.of(hotbar[itemInhHandIndex].getItem()); + } - return Optional.empty(); + public void decreaseItemInHand() { + if (hotbar[itemInhHandIndex] == null) { + return; + } + + if (hotbar[itemInhHandIndex].getAmount() == 1) { + hotbar[itemInhHandIndex] = null; + return; + } + + hotbar[itemInhHandIndex].setAmount(hotbar[itemInhHandIndex].getAmount() - 1); + } + + private void placeItem(Item item) { + var inventoryItem = new InventoryItem(item); + + // Try to place to hotbar + for (int i = 0; i < hotbar.length; i++) { + if (hotbar[i] == null) { + hotbar[i] = inventoryItem; + return; + } + } + + // Try to place to inventory + for (int i = 0; i < items.length; i++) { + if (items[i] == null) { + items[i] = inventoryItem; + break; + } + } + + // If inventory is full the item is lost } public void addItem(Item item) { if (!item.isStackable()) { - items.add(new InventoryItem(item)); + placeItem(item); return; } - var it = items.stream().filter(i -> i.getItem().equals(item)).toList(); - if (!it.isEmpty()) { - it.get(0).setAmount(it.get(0).getAmount() + 1); - - return; + // Try to stack in hotbar + for (InventoryItem value : hotbar) { + if (value != null && value.getItem().equals(item) && value.getAmount() < item.getStackAmount()) { + value.setAmount(value.getAmount() + 1); + return; + } } - items.add(new InventoryItem(item)); + // Try to stack in inventory + for (InventoryItem inventoryItem : items) { + if (inventoryItem != null && inventoryItem.getItem().equals(item) && inventoryItem.getAmount() < item.getStackAmount()) { + inventoryItem.setAmount(inventoryItem.getAmount() + 1); + return; + } + } + + // If stacking didn't work try to add it to inventory + placeItem(item); } private Number.NumberState getNumberState(int digit) { @@ -77,16 +134,47 @@ public class Inventory { return SpriteCombiner.combineTwoSprites( Number.fixForHotbar(nm.getSprite(numberState1), 2), Number.fixForHotbar(nm.getSprite(numberState2), 1)); } - public void renderHotbar(StringBuilder buffer, SpriteList spriteList, Terminal terminal) { + private String getHotbarBackground() { + StringBuilder sprite = new StringBuilder(); + for (int i = 0; i < 25; i++) { + sprite.append("\033[38;5;245;48;5;245m▓".repeat(50)); + sprite.append("\n"); + } + return sprite.toString(); + } + + public void renderHotbar(StringBuilder buffer, SpriteList spriteList, Terminal terminal, boolean isFull) { int termWidth = terminal.getWidth(); int startLeft = (termWidth / 2) - (INVENTORY_SIZE_PX / 2); - List sprites = items.subList(0, Math.min(9, items.size())).stream().map( - items -> SpriteCombiner.combineTwoSprites( - spriteList.getSprite(items.getItem().getSprite()).getSprite(), - getNumberSprite(items.getAmount()) - ) - ).toList(); + List sprites = new ArrayList<>(); + + for (int i = 0; i < hotbar.length; i++) { + var item = hotbar[i]; + + if (item == null) { + if (i == itemInhHandIndex && !isFull) { + sprites.add(getHotbarBackground()); + continue; + } + + sprites.add(null); + continue; + } + + if (i == (isFull ? selectedItemInv - 20 : itemInhHandIndex)) { + sprites.add(SpriteCombiner.combineTwoSprites(getHotbarBackground(), SpriteCombiner.combineTwoSprites( + spriteList.getSprite(item.getItem().getSprite()).getSprite(), + getNumberSprite(item.getAmount()) + ))); + continue; + } + + sprites.add(SpriteCombiner.combineTwoSprites( + spriteList.getSprite(item.getItem().getSprite()).getSprite(), + getNumberSprite(item.getAmount()) + )); + } for (int i = 0; i < 26; i++) { // Empty left space @@ -97,7 +185,7 @@ public class Inventory { } else { for (int j = 0; j < 9; j++) { buffer.append("\033[38;5;231;48;5;231m▓".repeat(2)); - if (sprites.size() <= j) { + if (sprites.get(j) == null) { buffer.append("\033[0m ".repeat(50)); continue; } @@ -110,4 +198,143 @@ public class Inventory { buffer.append("\033[0m\n"); } } + + public void renderFull(StringBuilder buffer, Terminal terminal, SpriteList spriteList) { + int widthPixels = COLUMN_AMOUNT * (50 + 4) + 2; + int heightPixels = ROW_AMOUNT * (25 + 1); + + int moveLeft = (terminal.getWidth() / 2) - (widthPixels / 2); + int moveTop = (terminal.getHeight() / 2) - (heightPixels / 2); + + List sprites = new ArrayList<>(); + + for (int i = 0; i < items.length; i++) { + var item = items[i]; + + if (item == null) { + if (i == selectedItemInv) { + sprites.add(getHotbarBackground()); + continue; + } + + sprites.add(null); + continue; + } + + if (i == selectedItemInv) { + sprites.add(SpriteCombiner.combineTwoSprites(getHotbarBackground(), SpriteCombiner.combineTwoSprites( + spriteList.getSprite(item.getItem().getSprite()).getSprite(), + getNumberSprite(item.getAmount()) + ))); + continue; + } + + sprites.add(SpriteCombiner.combineTwoSprites( + spriteList.getSprite(item.getItem().getSprite()).getSprite(), + getNumberSprite(item.getAmount()) + )); + } + + // Top center + buffer.append("\n".repeat(Math.max(0, moveTop))); + + buffer.append("\033[0m ".repeat(moveLeft)); + buffer.append("\033[38;5;231;48;5;231m▓".repeat(widthPixels)).append("\033[0m\n"); + + + for (int i = 0; i < ROW_AMOUNT; i++) { + for (int j = 0; j < 25; j++) { + buffer.append("\033[0m ".repeat(moveLeft)); + buffer.append("\033[38;5;231;48;5;231m▓"); + for (int k = 0; k < COLUMN_AMOUNT; k++) { + buffer.append("\033[38;5;231;48;5;231m▓".repeat(2)); + var item = items[i * COLUMN_AMOUNT + k]; + if (item == null) { + buffer.append("\033[0m ".repeat(50)); + buffer.append("\033[38;5;231;48;5;231m▓".repeat(2)); + continue; + } + var sprite = sprites.get(i * COLUMN_AMOUNT + k).split("\n"); + buffer.append(sprite[j]); + buffer.append("\033[38;5;231;48;5;231m▓".repeat(2)); + } + buffer.append("\033[38;5;231;48;5;231m▓"); + buffer.append("\n"); + } + buffer.append("\033[0m ".repeat(moveLeft)); + buffer.append("\033[38;5;231;48;5;231m▓".repeat(widthPixels)).append("\n"); + } + + buffer.append("\n".repeat(10)); + + renderHotbar(buffer, spriteList, terminal, true); + } + + public void click(MouseEvent mouseEvent, Terminal terminal, ScreenRenderer screenRenderer, Game game) { + if (mouseEvent.getType() != MouseEvent.Type.Pressed) { + return; + } + + int x = mouseEvent.getX(); + int y = mouseEvent.getY(); + + int widthPixels = COLUMN_AMOUNT * (50 + 4) + 2; + int heightPixels = ROW_AMOUNT * (25 + 1); + + int moveLeft = (terminal.getWidth() / 2) - (widthPixels / 2); + int moveTop = (terminal.getHeight() / 2) - (heightPixels / 2); + + int fx = x - moveLeft; + int fy = y - moveTop; + + int startLeftHotbar = (terminal.getWidth() / 2) - (INVENTORY_SIZE_PX / 2); + int startTopHotbar = moveTop + heightPixels + 10; + + if (x >= startLeftHotbar && y >= startTopHotbar && x <= startLeftHotbar + INVENTORY_SIZE_PX && y <= startTopHotbar + 26) { + int index = (x - startLeftHotbar) / 51; + if (hotbar[index] == null && selectedItemInv != -1) { + if (selectedItemInv < 20) { + hotbar[index] = items[selectedItemInv]; + items[selectedItemInv] = null; + } else { + int in = selectedItemInv - 20; + hotbar[index] = hotbar[in]; + hotbar[in] = null; + } + } else if (hotbar[index] != null) { + selectedItemInv = 20 + index; + } + + screenRenderer.render(game); + return; + } + + if (!(fx >= 0 && fx <= widthPixels && fy >=0 && fy <= heightPixels)) { + return; + } + + switch (mouseEvent.getButton()) { + case Button1 -> { + int blockX = fx / 54; + int blockY = fy / 26; + + InventoryItem inventoryItem = items[blockY * COLUMN_AMOUNT + blockX]; + if (inventoryItem != null) { + selectedItemInv = blockY * COLUMN_AMOUNT + blockX; + } + if (inventoryItem == null && selectedItemInv != -1) { + if (selectedItemInv < 20) { + items[blockY * COLUMN_AMOUNT + blockX] = items[selectedItemInv]; + items[selectedItemInv] = null; + } else { + int index = selectedItemInv - 20; + items[blockY * COLUMN_AMOUNT + blockX] = hotbar[index]; + hotbar[index] = null; + } + } + } + } + + screenRenderer.render(game); + } } diff --git a/src/main/java/cz/jzitnik/game/MouseHandler.java b/src/main/java/cz/jzitnik/game/MouseHandler.java index 613192e..e030090 100644 --- a/src/main/java/cz/jzitnik/game/MouseHandler.java +++ b/src/main/java/cz/jzitnik/game/MouseHandler.java @@ -1,5 +1,6 @@ package cz.jzitnik.game; +import cz.jzitnik.game.sprites.Window; import cz.jzitnik.tui.ScreenMovingCalculationProvider; import cz.jzitnik.tui.ScreenRenderer; import lombok.AllArgsConstructor; @@ -22,14 +23,21 @@ public class MouseHandler { return; } - if (mouseEvent.getButton() == MouseEvent.Button.Button2 && mouseEvent.getType() == MouseEvent.Type.Pressed) { + if (mouseEvent.getButton() == MouseEvent.Button.Button3 && mouseEvent.getType() == MouseEvent.Type.Pressed) { rightClick(mouseEvent); return; } if (mouseEvent.getType() == MouseEvent.Type.Moved) { move(mouseEvent); + return; } + + if (mouseEvent.getType() == MouseEvent.Type.Wheel) { + scroll(mouseEvent); + } + } + private void scroll(MouseEvent mouseEvent) { } private void leftClick(MouseEvent mouseEvent) { @@ -78,7 +86,7 @@ public class MouseHandler { } private void move(MouseEvent mouseEvent) { - if (game.isMining()) { + if (game.isMining() || game.getWindow() != Window.WORLD) { return; } diff --git a/src/main/java/cz/jzitnik/game/SpriteLoader.java b/src/main/java/cz/jzitnik/game/SpriteLoader.java index 5b253a7..79e5774 100644 --- a/src/main/java/cz/jzitnik/game/SpriteLoader.java +++ b/src/main/java/cz/jzitnik/game/SpriteLoader.java @@ -1,7 +1,7 @@ package cz.jzitnik.game; import cz.jzitnik.game.sprites.*; -import cz.jzitnik.game.sprites.items.DirtItem; +import cz.jzitnik.game.sprites.items.SimpleItem; import cz.jzitnik.tui.Sprite; import cz.jzitnik.tui.SpriteList; @@ -21,7 +21,8 @@ public class SpriteLoader { OAK_LEAF, // Items - ITEM_DIRT + ITEM_DIRT, + ITEM_OAK_LOG } public static final HashMap SPRITES_MAP = new HashMap<>(); @@ -36,7 +37,9 @@ public class SpriteLoader { SPRITES_MAP.put(SPRITES.BREAKING, new Breaking()); SPRITES_MAP.put(SPRITES.OAK_LOG, new OakLog()); SPRITES_MAP.put(SPRITES.OAK_LEAF, new OakLeaf()); - SPRITES_MAP.put(SPRITES.ITEM_DIRT, new DirtItem()); + + SPRITES_MAP.put(SPRITES.ITEM_DIRT, new SimpleItem("dirt.ans")); + SPRITES_MAP.put(SPRITES.ITEM_OAK_LOG, new SimpleItem("oak_log.ans")); } 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 index d3d7fc3..072e4f7 100644 --- a/src/main/java/cz/jzitnik/game/items/Item.java +++ b/src/main/java/cz/jzitnik/game/items/Item.java @@ -5,12 +5,12 @@ import cz.jzitnik.game.SpriteLoader; import lombok.AllArgsConstructor; import lombok.Getter; -import java.util.Objects; import java.util.Optional; @Getter @AllArgsConstructor public class Item { + private String id; private String name; private ItemType type; private Optional toolVariant = Optional.empty(); @@ -18,9 +18,11 @@ public class Item { private boolean stackable = true; private int durability; private int miningDecrease = 0; + private int stackAmount = 64; private Optional block; - public Item(String name, ItemType type, SpriteLoader.SPRITES sprite, Block block) { + public Item(String id, String name, ItemType type, SpriteLoader.SPRITES sprite, Block block) { + this.id = id; this.name = name; this.type = type; this.sprite = sprite; @@ -36,11 +38,7 @@ public class Item { if (this == o) return true; if (o == null || getClass() != o.getClass()) return false; Item item = (Item) o; - return stackable == item.stackable && - durability == item.durability && - miningDecrease == item.miningDecrease && - Objects.equals(name, item.name) && - type == item.type && - Objects.equals(sprite, item.sprite); + + return name.equals(item.name); } } diff --git a/src/main/java/cz/jzitnik/game/sprites/Window.java b/src/main/java/cz/jzitnik/game/sprites/Window.java new file mode 100644 index 0000000..ce08d72 --- /dev/null +++ b/src/main/java/cz/jzitnik/game/sprites/Window.java @@ -0,0 +1,6 @@ +package cz.jzitnik.game.sprites; + +public enum Window { + WORLD, + INVENTORY +} diff --git a/src/main/java/cz/jzitnik/game/sprites/items/DirtItem.java b/src/main/java/cz/jzitnik/game/sprites/items/DirtItem.java deleted file mode 100644 index 173a7df..0000000 --- a/src/main/java/cz/jzitnik/game/sprites/items/DirtItem.java +++ /dev/null @@ -1,14 +0,0 @@ -package cz.jzitnik.game.sprites.items; - -import cz.jzitnik.tui.ResourceLoader; -import cz.jzitnik.tui.Sprite; - -public class DirtItem extends Sprite { - public String getSprite() { - return ResourceLoader.loadResource("items/dirt.ans"); - } - - public String getSprite(Enum key) { - throw new RuntimeException("Imposible state"); - } -} diff --git a/src/main/java/cz/jzitnik/game/sprites/items/SimpleItem.java b/src/main/java/cz/jzitnik/game/sprites/items/SimpleItem.java new file mode 100644 index 0000000..16a1550 --- /dev/null +++ b/src/main/java/cz/jzitnik/game/sprites/items/SimpleItem.java @@ -0,0 +1,20 @@ +package cz.jzitnik.game.sprites.items; + +import cz.jzitnik.tui.ResourceLoader; +import cz.jzitnik.tui.Sprite; + +public class SimpleItem extends Sprite { + private String resource; + + public SimpleItem(String resource) { + this.resource = resource; + } + + public String getSprite() { + return ResourceLoader.loadResource("items/" + resource).replaceAll("\033\\[38;5;1;48;5;16m", "\033[0m"); + } + + public String getSprite(Enum key) { + throw new RuntimeException("Imposible state"); + } +} diff --git a/src/main/java/cz/jzitnik/game/sprites/ui/Font.java b/src/main/java/cz/jzitnik/game/sprites/ui/Font.java new file mode 100644 index 0000000..167a7e6 --- /dev/null +++ b/src/main/java/cz/jzitnik/game/sprites/ui/Font.java @@ -0,0 +1,27 @@ +package cz.jzitnik.game.sprites.ui; + +import cz.jzitnik.tui.ResourceLoader; +import cz.jzitnik.tui.Sprite; + +public class Font extends Sprite { + public enum FontState { + A,B,C,D,E,F,G,H,I,J,K, + L,M,N,O,P,Q,R,S,T,U,V, + W,X,Y,Z,a,b,c,d,e,f,g, + h,i,j,k,l,m,n,o,p,q,r, + s,t,u,v,w,x,y,z + } + + + public String getSprite() { + throw new RuntimeException("Invalid state"); + } + + public String getSprite(Enum key) { + // TODO: Implement font + + String fullSprite = ResourceLoader.loadResource("ui/font.ans"); + + return null; + } +} diff --git a/src/main/java/cz/jzitnik/game/sprites/Number.java b/src/main/java/cz/jzitnik/game/sprites/ui/Number.java similarity index 98% rename from src/main/java/cz/jzitnik/game/sprites/Number.java rename to src/main/java/cz/jzitnik/game/sprites/ui/Number.java index 21c959d..7c18556 100644 --- a/src/main/java/cz/jzitnik/game/sprites/Number.java +++ b/src/main/java/cz/jzitnik/game/sprites/ui/Number.java @@ -1,4 +1,4 @@ -package cz.jzitnik.game.sprites; +package cz.jzitnik.game.sprites.ui; import cz.jzitnik.tui.ResourceLoader; import cz.jzitnik.tui.Sprite; diff --git a/src/main/java/cz/jzitnik/tui/ScreenRenderer.java b/src/main/java/cz/jzitnik/tui/ScreenRenderer.java index 84a3699..20205ff 100644 --- a/src/main/java/cz/jzitnik/tui/ScreenRenderer.java +++ b/src/main/java/cz/jzitnik/tui/ScreenRenderer.java @@ -41,96 +41,103 @@ public class ScreenRenderer { StringBuilder main = new StringBuilder(); main.append("\033[H\033[2J"); - int[] cords = getPlayerCords(world); - if (cords == null) return; + switch (game.getWindow()) { + case INVENTORY -> game.getInventory().renderFull(main, terminal, spriteList); + case WORLD -> { + // World - int playerX = cords[0]; - int playerY = cords[1]; + int[] cords = getPlayerCords(world); + if (cords == null) return; - int terminalWidth = terminal.getWidth(); - int terminalHeight = terminal.getHeight(); + int playerX = cords[0]; + int playerY = cords[1]; - int[] data = ScreenMovingCalculationProvider.calculate(playerX, playerY, terminalHeight, terminalWidth, world[0].length, world.length); + int terminalWidth = terminal.getWidth(); + int terminalHeight = terminal.getHeight(); - // Calculate visible area boundaries - int startX = data[0]; - int endX = data[1]; - int startY = data[2]; - int endY = data[3]; + int[] data = ScreenMovingCalculationProvider.calculate(playerX, playerY, terminalHeight, terminalWidth, world[0].length, world.length); - int visibleWidth = endX - startX; - int visibleHeight = endY - startY; + // Calculate visible area boundaries + int startX = data[0]; + int endX = data[1]; + int startY = data[2]; + int endY = data[3]; - // If the width is odd, reduce viewXRadius by 1 - if (visibleWidth % 2 != 0) { - endX = Math.max(startX, endX - 1); - } - if (visibleHeight % 2 != 0) { - endY = Math.max(startY, endY - 1); - } + int visibleWidth = endX - startX; + int visibleHeight = endY - startY; - StringBuilder[] lines = new StringBuilder[(endY - startY) * 25 + 1]; - for (int i = 0; i < lines.length; i++) { - lines[i] = new StringBuilder(); - } + // If the width is odd, reduce viewXRadius by 1 + if (visibleWidth % 2 != 0) { + endX = Math.max(startX, endX - 1); + } + if (visibleHeight % 2 != 0) { + endY = Math.max(startY, endY - 1); + } - int counter = 0; - for (int y = startY; y < endY; y++) { - for (int x = startX; x < endX; x++) { - List blocks = world[y][x]; - List sprites = new ArrayList<>(blocks.stream() - .map(block -> - block.getSpriteState().isEmpty() ? - spriteList.getSprite(block.getSprite()).getSprite() : - spriteList.getSprite(block.getSprite()).getSprite(block.getSpriteState().get()) - ).toList() - ); + StringBuilder[] lines = new StringBuilder[(endY - startY) * 25 + 1]; + for (int i = 0; i < lines.length; i++) { + lines[i] = new StringBuilder(); + } - if (selectedBlock.isPresent() && selectedBlock.get().get(0) == x && selectedBlock.get().get(1) == y) { - StringBuilder stringBuilder = new StringBuilder(); + int counter = 0; + for (int y = startY; y < endY; y++) { + for (int x = startX; x < endX; x++) { + List blocks = world[y][x]; + List sprites = new ArrayList<>(blocks.stream() + .map(block -> + block.getSpriteState().isEmpty() ? + spriteList.getSprite(block.getSprite()).getSprite() : + spriteList.getSprite(block.getSprite()).getSprite(block.getSpriteState().get()) + ).toList() + ); - stringBuilder.append("\033[38;5;231;48;5;231m▓".repeat(50)); - stringBuilder.append("\n"); + if (selectedBlock.isPresent() && selectedBlock.get().get(0) == x && selectedBlock.get().get(1) == y) { + StringBuilder stringBuilder = new StringBuilder(); - for (int i = 0; i < 23; i++) { - stringBuilder.append("\033[38;5;231;48;5;231m▓"); - stringBuilder.append("\033[38;5;231;48;5;231m▓"); - stringBuilder.append("\033[0m ".repeat(46)); - stringBuilder.append("\033[38;5;231;48;5;231m▓"); - stringBuilder.append("\033[38;5;231;48;5;231m▓"); - stringBuilder.append("\n"); + stringBuilder.append("\033[38;5;231;48;5;231m▓".repeat(50)); + stringBuilder.append("\n"); + + for (int i = 0; i < 23; i++) { + stringBuilder.append("\033[38;5;231;48;5;231m▓"); + stringBuilder.append("\033[38;5;231;48;5;231m▓"); + stringBuilder.append("\033[0m ".repeat(46)); + stringBuilder.append("\033[38;5;231;48;5;231m▓"); + stringBuilder.append("\033[38;5;231;48;5;231m▓"); + stringBuilder.append("\n"); + } + + stringBuilder.append("\033[38;5;231;48;5;231m▓".repeat(50)); + stringBuilder.append("\n"); + + sprites.add(stringBuilder.toString()); + } + + String sprite = SpriteCombiner.combineSprites(sprites.toArray(String[]::new)); + + String[] spriteLines = sprite.split("\n"); + + for (int i = 0; i < spriteLines.length; i++) { + lines[counter * 25 + i].append(spriteLines[i]).append("\033[0m"); + } } - - stringBuilder.append("\033[38;5;231;48;5;231m▓".repeat(50)); - stringBuilder.append("\n"); - - sprites.add(stringBuilder.toString()); + counter++; } - String sprite = SpriteCombiner.combineSprites(sprites.toArray(String[]::new)); - - String[] spriteLines = sprite.split("\n"); - - for (int i = 0; i < spriteLines.length; i++) { - lines[counter * 25 + i].append(spriteLines[i]).append("\033[0m"); + // World + for (int i = 0; i < lines.length; i++) { + main.append(lines[i]); + if (i < lines.length - 1) { + main.append("\n"); + } } - } - counter++; - } - // World - for (int i = 0; i < lines.length; i++) { - main.append(lines[i]); - if (i < lines.length - 1) { - main.append("\n"); + // Empty space between world and hotbar + main.append("\n\n\n"); + + game.getInventory().renderHotbar(main, spriteList, terminal, false); } } - // Empty space between world and hotbar - main.append("\n\n\n"); - - game.getInventory().renderHotbar(main, spriteList, terminal); - System.out.println(main); } diff --git a/src/main/resources/textures/items/oak_log.ans b/src/main/resources/textures/items/oak_log.ans new file mode 100644 index 0000000..d944e40 --- /dev/null +++ b/src/main/resources/textures/items/oak_log.ans @@ -0,0 +1,26 @@ +                                                   +                      ▒▒▒▒▒▒▒                      +                  ▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒                  +              ▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒              +          ▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒          +      ▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒      +   ▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒░░   +   ▒▒░▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒░░▒▒░░   +   ▒▒ ▒▒▒▒ ▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒ ▒▒▒░░▒▒░░   +   ▒▒ ▒▒▒▒ ▒▒▒▒▒ ▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒ ▒▒░░░▒▒░░   +   ▒▒░▒▒▒▒ ▒▒▒▒▒ ▒▒▒░▒▒▒▒▒▒▒▒▒▒▒  ▒░░▒░▒▒ ░░░▒▒▒   +   ▒▒▒░▒▒▒ ▒▒▒▒▒░▒▒▒░▒▒▒▒▒▒░▒▒░▒  ▒░░▒░▒▒░▒▒░▒▒▒   +   ▒▒▒░▒▒▒░▒▒▒▒▒▒▒░░░▒▒▒▒▒▒░▒░░▒░░▒░░▒░▒▒░▒▒░▒▒▒   +   ▒▒▒ ▒▒▒▒▒▒░▒▒▒▒▒ ░▒▒▒▒▒▒ ▒▒░▒  ▒░░▒░▒▒▒▒▒░▒▒▒   +   ▒▒▒ ▒▒▒▒▒▒▒▒▒▒▒░░▒░░▒▒▒▒ ▒▒░▒░░▒▒▒▒▒▒▒░▒▒░ ▒▒   +   ▒▒▒░▒▒▒▒▒▒▒▒▒▒▒▒▒▒░░▒▒▒▒▒░░▒▒░░░▒▒▒░▒▒░▒▒▒ ▒▒   +   ▒▒░▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒░░▒▒▒▒▒░░▒▒▒▒░▒▒▒░▒▒░▒▒▒░▒▒   +   ▒▒░▒▒▒▒▒▒▒ ▒▒▒▒▒▒▒▒▒▒▒░░▒  ▒▒▒▒░▒ ▒░▒▒ ▒▒▒░▒▒   +   ▒▒▒░▒▒▒▒▒▒ ▒▒▒▒░░▒▒▒▒▒░▒░ ░▒▒░░▒  ▒▒▒▒ ▒▒▒▒▒▒   +   ▒▒▒░▒▒▒▒▒▒ ▒▒▒▒ ░▒▒▒ ▒▒▒▒▒▒▒▒░░▒  ▒░▒▒░░░▒▒░░   +      ░▒▒▒▒▒▒░▒▒▒▒  ▒▒▒ ▒▒▒▒▒▒▒▒░░▒░░▒░▒▒▒░░▒      +          ▒▒▒ ▒▒░▒░░▒▒▒▒▒▒▒░▒▒▒▒▒▒▒  ▒░▒▒          +              ▒▒░▒░░░▒▒▒▒▒▒▒░░▒▒▒▒▒░░              +                  ▒▒░▒▒▒▒▒▒▒░░▒░▒                  +                      ▒▒▒▒▒▒░                      + \ No newline at end of file diff --git a/src/main/resources/textures/ui/font.ans b/src/main/resources/textures/ui/font.ans new file mode 100644 index 0000000..1679125 --- /dev/null +++ b/src/main/resources/textures/ui/font.ans @@ -0,0 +1,48 @@ +    ▓▓▓▓       ▓▓▓▓▓▓▓▓▓▓       ▓▓▓▓▓▓▓▓   ▓▓▓▓▓▓▓▓▓▓▓   ▓▓▓▓▓▓▓▓▓▓▓▓░ ▓▓▓▓▓▓▓▓▓▓▓▓▓     ▓▓▓▓▓▓▓    ▓▓▓    ▓▓▓     ▓▓▓▓▓▓▓▓          ▓▓▓▓▓▓▓  ▓▓▓▓▓    ▓▓▓ +  ▓▓▓▓▓▓▓       ▓▓▓▓   ░▓▓▓   ▒▓▓▓▓   ▓▓▓    ▓▓▓    ▓▓▓    ▓▓▓      ▓░   ▓▓▓▓     ▓▓   ▓▓▓▓▓  ▓▓▓▓  ▓▓▓    ▓▓▓       ▓▓▓▓              ▓▓▓░     ▓▓▓    ▓▓▓ +▓▓▓▓   ░▓▓▓     ▓▓▓▓   ░▓▓▓  ▓▓▓▓            ▓▓▓    ▓▓▓    ▓▓▓  ▓▓       ▓▓▓▓ ▓▓     ▓▓▓▓▓          ▓▓▓    ▓▓▓       ▓▓▓▓              ▓▓▓░     ▓▓▓  ▓▓▓▓▓ +▓▓▓▓   ░▓▓▓     ▓▓▓▓▓▓▓▓▓    ▓▓▓             ▓▓▓    ▓▓▓    ▓▓▓▓▓▓▓       ▓▓▓▓▓▓▓     ▓▓▓▓           ▓▓▓▓▓▓▓▓▓▓       ▓▓▓▓              ▓▓▓░     ▓▓▓▓▓▓▓▓   +▓▓▓▓▓▓▓▓▓▓▓     ▓▓▓▓   ░▓▓▓  ▓▓▓▓            ▓▓▓    ▓▓▓    ▓▓▓  ▓▓       ▓▓▓▓ ▓▓     ▓▓▓▓▓  ▓▓▓▓▓▓  ▓▓▓    ▓▓▓       ▓▓▓▓       ▓▓▓▒   ▓▓▓░     ▓▓▓  ▓▓▓▓▓ +▓▓▓▓   ░▓▓▓     ▓▓▓▓   ░▓▓▓   ▒▓▓▓▓   ▓▓▓    ▓▓▓    ▓▓▓    ▓▓▓      ▓░   ▓▓▓▓          ▓▓▓▓▓  ▓▓▓▓  ▓▓▓    ▓▓▓       ▓▓▓▓       ▓▓▓▒   ▓▓▓░     ▓▓▓    ▓▓▓ +▓▓▓▓   ░▓▓▓    ▓▓▓▓▓▓▓▓▓▓       ▓▓▓▓▓▓▓▓   ▓▓▓▓▓▓▓▓▓▓▓   ▓▓▓▓▓▓▓▓▓▓▓▓░ ▓▓▓▓▓▓▓           ▓▓▓▓▓▓▓▓▓  ▓▓▓    ▓▓▓     ▓▓▓▓▓▓▓▓       ▓▓▓▓▓▓▓     ▓▓▓▓▓    ▓▓▓ +                                                                                                                                                           +▓▓▓▓▓▓▓▓       ▓▓▓     ░▓▓▓  ▓▓▓      ▓▓▓     ░▓▓▓▓▓     ▓▓▓▓▓▓▓▓▓▓▓     ▓▓▓▓▓▓▓     ▓▓▓▓▓▓▓▓▓▓▓     ▓▓▓▓▓▓▓▒     ▓▓▓▓▓▓▓▓▓▓    ▓▓▓▒   ▓▓▓░   ▓▓▓▓   ▓▓▓▓  +  ▓▓▓▓         ▓▓▓▓▓  ▓▓▓▓▓  ▓▓▓▓▓    ▓▓▓    ▓▓▓  ▓▓▓▓     ▓▓▓    ▓▓▓░ ▓▓▓▓   ▓▓▓▓     ▓▓▓▓   ▓▓▓▓  ▓▓▓    ▓▓▓    ▓░ ▓▓▓▓  ▓    ▓▓▓▒   ▓▓▓░   ▓▓▓▓   ▓▓▓▓  +  ▓▓▓▓         ▓▓▓▓▓▓▓▓▓▓▓▓  ▓▓▓▓▓▓▓  ▓▓▓  ▓▓▓░     ▓▓▓    ▓▓▓    ▓▓▓░ ▓▓▓▓   ▓▓▓▓     ▓▓▓▓   ▓▓▓▓  ▓▓▓▓▓            ▓▓▓▓       ▓▓▓▒   ▓▓▓░   ▓▓▓▓   ▓▓▓▓  +  ▓▓▓▓         ▓▓▓ ▓▓▓▓░▓▓▓  ▓▓▓ ▓▓▓▓▓▓▓▓  ▓▓▓░     ▓▓▓    ▓▓▓▓▓▓▓▓▓▓  ▓▓▓▓   ▓▓▓▓     ▓▓▓▓▓▓▓▓▓▓   ░▓▓▓▓▓▓▓▒        ▓▓▓▓       ▓▓▓▒   ▓▓▓░   ▓▓▓▓   ▓▓▓▓  +  ▓▓▓▓    ▓▓▓  ▓▓▓     ░▓▓▓  ▓▓▓   ▒▓▓▓▓▓  ▓▓▓░     ▓▓▓    ▓▓▓         ▓▓▓▓ ▓▓▓▓▓▓     ▓▓▓▓▓▓▓▓         ▒▓▓▓▓▓       ▓▓▓▓       ▓▓▓▒   ▓▓▓░   ▓▓▓▓   ▓▓▓▓  +  ▓▓▓▓   ▓▓▓▓  ▓▓▓     ░▓▓▓  ▓▓▓      ▓▓▓    ▓▓▓  ▓▓▓▓     ▓▓▓           ▓▓▓▓▓▓▓       ▓▓▓▓ ▓▓▓▓▓   ▓▓▓    ▓▓▓       ▓▓▓▓       ▓▓▓▒   ▓▓▓░     ▓▓▓▓▓▓▓    +▓▓▓▓▓▓▓▓▓▓▓▓▓  ▓▓▓     ░▓▓▓  ▓▓▓      ▓▓▓     ░▓▓▓▓▓     ▓▓▓▓▓▓▓            ▓▓▓▓▓▓   ▓▓▓▓▓▓   ▓▓▓▓   ▓▓▓▓▓▓▓▒      ▓▓▓▓▓▓▓▓     ▓▓▓▓▓▓▓▓▓▓░      ▓▓▓▓      +                                                                                                                                                           +▓▓▓▓     ▓▓▓▓  ▓▓▓     ░▓▓▓  ▓▓▓    ▓▓▓▓   ▓▓▓▓▓▓▓▓▓▓▓▓                ▓▓▓▓▓▓                            ▓▓▓▓▓                     ▓▓▓▓▓▓                  +▓▓▓▓     ▓▓▓▓  ▓▓▓     ░▓▓▓  ▓▓▓    ▓▓▓▓   ▓▓▓░   ▓▓▓▓▒                  ▓▓▓▓                              ▓▓▓                    ▓▓▓  ▓▓▓░                +▓▓▓▓     ▓▓▓▓   ▓▓▓▓  ▓▓▓▓   ▓▓▓    ▓▓▓▓   ▓▓▓  ▓▓▓▓▓      ▓▓▓▓▓▓▓       ▓▓▓▓▓▓▓▓▓     ▓▓▓▓▓▓▓             ▓▓▓     ▓▓▓▓▓▓▓▓       ▓▓▓           ▓▓▓▓▓▓ ▓▓▓ +▓▓▓▓ ░▓▓▓▓▓▓▓     ▓▓▓▓▓▒      ░▓▓▓▓▓▓▓░      ▒▓▓▓▓░             ▓▓▓▓     ▓▓▓▓   ▓▓▓▓ ▓▓▓▓   ▓▓▓▓    ░▓▓▓▓▓▓▓▓▓    ▓▓▓    ▓▓▓    ▓▓▓▓▓▓▓       ▓▓▓▓   ▓▓▓▓  +▓▓▓▓▓▓▓▓▓▓▓▓▓   ▓▓▓▓  ▓▓▓▓      ▓▓▓▓        ▓▓▓▓     ▓▓   ▓▓▓▓▓▓▓▓▓▓     ▓▓▓▓   ▓▓▓▓ ▓▓▓▓           ▓▓▓    ▓▓▓    ▓▓▓▓▓▓▓▓▓▓      ▓▓▓         ▓▓▓▓   ▓▓▓▓  +▓▓▓▓▓▓ ░▓▓▓▓▓  ▓▓▓     ░▓▓▓     ▓▓▓▓       ▓▓▓░     ▓▓▓  ▓▓▓▓   ▓▓▓▓     ▓▓▓▓   ▓▓▓▓ ▓▓▓▓   ▓▓▓▓    ▓▓▓    ▓▓▓    ▓▓▓             ▓▓▓           ▓▓▓▓▓▓▓▓▓  +▓▓▓▓     ▓▓▓▓  ▓▓▓     ░▓▓▓   ▒▓▓▓▓▓▓▓     ▓▓▓▓▓▓▓▓▓▓▓▓    ▓▓▓▓▓▓ ▓▓▓░ ▓▓▓▓ ▓▓▓▓▓▓     ▓▓▓▓▓▓▓       ▓▓▓▓▓▓░▓▓▓▓   ▓▓▓▓▓▓▓▓     ▓▓▓▓▓▓▓              ▓▓▓▓  +                                                                                                                                              ▒▒▒░▒▒▒▒▒    +▓▓▓▓▓▓            ▓▓▓▓            ▓▓▓▓     ▓▓▓▓▓           ▓▓▓▓▓                                                                                           +  ▓▓▓▓                                       ▓▓▓             ▓▓▓                                                                                           +  ▓▓▓▓ ░▓▓▓     ▓▓▓▓▓▓        ▒▓▓▓▓▓▓▓       ▓▓▓    ▓▓▓      ▓▓▓       ▓▓▓▓▓▓ ▓▓▓▓   ▓▓▓▓▓▓▓▓▓       ▓▓▓▓▓▓▓▒     ▓▓▓ ▓▓▓▓▓▓      ▓▓▓▓▓▓ ▓▓▓  ▓▓▓▓░▓▓▓▓    +  ▓▓▓▓▓▓▓▓▓▓▓     ▓▓▓▓            ▓▓▓▓       ▓▓▓  ▓▓▓▓▓      ▓▓▓       ▓▓▓▓▓▓▓▓▓▓▓▓▓ ▓▓▓▓   ▓▓▓▓    ▓▓▓    ▓▓▓     ▓▓▓▓    ▓▓▓  ▓▓▓▒   ▓▓▓░     ▓▓▓  ▓▓▓▓  +  ▓▓▓▓   ▓▓▓▓     ▓▓▓▓            ▓▓▓▓       ▓▓▓▓▓▓▓▓        ▓▓▓       ▓▓▓▓ ▓▓  ▓▓▓▓ ▓▓▓▓   ▓▓▓▓    ▓▓▓    ▓▓▓     ▓▓▓▓    ▓▓▓  ▓▓▓▒   ▓▓▓░     ▓▓▓  ▓▓▓▓  +  ▓▓▓▓   ▓▓▓▓     ▓▓▓▓            ▓▓▓▓       ▓▓▓  ▓▓▓▓▒      ▓▓▓       ▓▓▓▓     ▓▓▓▓ ▓▓▓▓   ▓▓▓▓    ▓▓▓    ▓▓▓     ▓▓▓▓▓▓▓▓▓      ▓▓▓▓▓▓▓▓░     ▓▓▓        +▓▓▓▓▓▓   ▓▓▓▓   ▓▓▓▓▓▓▓▒     ▓▓▓  ▓▓▓▓     ▓▓▓▓▓    ▓▓▓    ▓▓▓▓▓▓▓     ▓▓▓▓     ▓▓▓▓ ▓▓▓▓   ▓▓▓▓     ▓▓▓▓▓▓▓▒      ▓▓▓▓                ▓▓▓░   ▓▓▓▓▓▓▓      +                              ▓░░░░░                                                                              ░░░░░░░            ░░░░░░░               +                    ▓▓                                                                                             ▓▓▓▓▓▓▓▓        ▓▓▓▓         ▓▓▓▓▓▓▓    +                  ▓▓▓▓                                                                                            ▓▓▓    ▓▓▓    ▓▓▓▓▓▓▓       ▓▓▓▓   ▓▓▓▓  + ░▓▓▓▓▓▓▓▓▓     ▓▓▓▓▓▓▓▓▓    ▓▓▓    ▓▓▓▓   ▓▓▓░   ▓▓▓▓   ▓▓▓▓     ▓▓▓░ ▓▓▓▓░   ░▓▓▓▓ ▓▓▓▓   ▓▓▓▓    ▓▓▓▓▓▓▓▓▓▓    ▓▓▓ ░▓▓▓▓▓       ▓▓▓▓             ░▓▓▓▓  +▓▓▓▓              ▓▓▓▓       ▓▓▓    ▓▓▓▓   ▓▓▓░   ▓▓▓▓   ▓▓▓▓     ▓▓▓░  ▒▓▓▓▓▓▓▓▓▓▒  ▓▓▓▓   ▓▓▓▓    ▓▒  ▓▓▓▓░     ▓▓▓▓▓▓▓▓▓▓       ▓▓▓▓          ▓▓▓▓▓▓▒   +  ▓▓▓▓▓▓▓▓        ▓▓▓▓       ▓▓▓    ▓▓▓▓   ▓▓▓░   ▓▓▓▓   ▓▓▓▓░▓▓▓ ▓▓▓░    ▓▓▓▓▓▓▓    ▓▓▓▓   ▓▓▓▓      ▓▓▓▓▓       ▓▓▓▓▓  ▓▓▓       ▓▓▓▓        ▓▓▓▓        +       ░▓▓▓       ▓▓▓▓ ░▓    ▓▓▓    ▓▓▓▓     ▓▓▓▓▓▓▓     ▓▓▓▓▓▓▓▓▓▓▓▓░  ▓▓▓▓▓ ▓▓▓▓▓    ▓▓▓▓▓▓▓▓▓    ░▓▓▓▓  ░▓▓    ▓▓▓    ▓▓▓       ▓▓▓▓       ▓▓▓▓   ▓▓▓▓  +▓▓▓▓▓▓▓▓▓           ▓▓▓▒      ▒▓▓▓▓▓▓ ▓▓▓     ░▓▓▓         ▓▓▓  ▓▓▓▓   ▓▓▓▓     ▓▓▓▓        ▓▓▓▓    ▓▓▓▓▓▓▓▓▓▓     ▓▓▓▓▓▓▓▓     ▓▓▓▓▓▓▓▓▓▓░   ▓▓▓▓▓▓▓▓▓▓▓  +                                                                                     ▒▒▒▒▒▒▒▒▒                                                             +  ▓▓▓▓▓▓▓           ▓▓▓▓▓    ▓▓▓▓▓▓▓▓▓▓▓      ░▓▓▓▓▓     ▓▓▓▓▓▓▓▓▓▓▓     ▓▓▓▓▓▓▓       ▓▓▓▓▓▓▓                                    ▓▓▓  ▓▓▓░     ▓▓▓  ▓▓▓▓  +▓▓▓▓   ░▓▓▓       ▓▓▓▓▓▓▓    ▓▓▓             ▓▓▓▓        ▓▓▓▓   ▓▓▓▓   ▓▓▓▓   ▓▓▓▓   ▓▓▓▓   ▓▓▓▓                                  ▓▓▓  ▓▓▓░     ▓▓▓  ▓▓▓▓  +       ░▓▓▓    ▒▓▓▓▓▓ ▓▓▓    ▓▓▓▓▓▓▓▓▓░    ▓▓▓▓                ░▓▓▓▓   ▓▓▓▓   ▓▓▓▓   ▓▓▓▓   ▓▓▓▓                                  ▓▓▓  ▓▓▓░     ▓▓▓  ▓▓▓▓  +    ▓▓▓▓▓      ▓▓▓▓   ▓▓▓           ▓▓▓▓   ▓▓▓▓▓▓▓▓▓▓        ░▓▓▓▓▓      ▓▓▓▓▓▓▓      ▓▓▓▓▓▓▓▓▓▓                                                           +       ░▓▓▓    ▓▓▓▓▓▓▓▓▓▓▓▓         ▓▓▓▓   ▓▓▓░   ▓▓▓▓      ▓▓▓▓       ▓▓▓▓   ▓▓▓▓          ▓▓▓▓                                                           +▓▓▓▓   ░▓▓▓           ▓▓▓    ▓▓▓    ▓▓▓▓   ▓▓▓░   ▓▓▓▓     ▓▓▓         ▓▓▓▓   ▓▓▓▓        ▓▓▓▓           ▓▓▓▒        ▓▓▓▓▓▓                                +  ░░░░░▓░             ░░░     ▒░░░░░░░       ░▓░░░░░       ░░░           ░░░░░░░       ░░░░░░            ░░░▒        ░░▓▓▓░                                + \ No newline at end of file