feat: Implemented breaking animation

This commit is contained in:
Jakub Žitník 2025-02-19 16:11:36 +01:00
parent a359bedd3a
commit 2d53047e75
Signed by: jzitnik
GPG Key ID: C577A802A6AF4EF3
12 changed files with 311 additions and 117 deletions

View File

@ -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<Enum> spriteState = Optional.empty();
private boolean ghost = false;
private int hardness = 1;
private Optional<ItemType> 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);
}
}

View File

@ -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<Block>[][] world = new ArrayList[50][50];
private List<Block>[][] world = new ArrayList[256][512];
private Block player;
private boolean mining;
private boolean mining = false;
private List<Item> inventory;
private Optional<Item> 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));
// 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));
}
world[49][i].add(new Block("bedrock", SpriteLoader.SPRITES.BEDROCK));
// 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;
}
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
// 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<Block>[] lists : world) {
for (List<Block> 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<Block> 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);
int startX = data[0];
int endX = data[1];
int startY = data[2];
int endY = data[3];
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);
}
if (world[y][x].stream().allMatch(Block::isGhost)) {
return;
}
world[y][x].clear();
world[y][x].add(new Block("air", SpriteLoader.SPRITES.AIR, true));
screenRenderer.render(world);
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();
}
}

View File

@ -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<Block> blocks = game.getWorld()[blockY][blockX];
System.out.println(blockX);
System.out.println(blockY);
if (!blocks.isEmpty()) {
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<Block> 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<Integer> 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());
}

View File

@ -13,7 +13,8 @@ public class SpriteLoader {
GRASS,
STEVE,
STONE,
BEDROCK
BEDROCK,
BREAKING
}
public static final HashMap<SPRITES, Sprite> 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<SPRITES> load() {

View File

@ -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;
}
}

View File

@ -0,0 +1,6 @@
package cz.jzitnik.game.items;
public enum ItemType {
PICKAXE,
SHOVEL
}

View File

@ -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);
});
}));
}
}

View File

@ -77,7 +77,12 @@ public class ScreenRenderer {
for (int x = startX; x < endX; x++) {
List<Block> blocks = world[y][x];
List<String> 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");
}

View File

@ -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();

View File

@ -4,19 +4,19 @@
                                                  
                                                  
                                                  
                                                  
                             ▓▓▓                  
                             ▓▓▓                  
                             ▓▓▓                  
             ▓▓▒             ▓▓▓▓▓▓▓▓▓            
         ░░░ ▓▓░             ▓▓▓▓▓▓▓▓▓            
         ▒▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓            
         ▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓                  
         ▒▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓                  
                   ▓▓▓                            
                   ▓▓▓                            
                   ▓▓▓                            
                   ▓▓▓                            
                             ▓▓▓                  
                            ▓▓▓                  
                            ▓▓▓                  
                            ▓▓▓                  
             ▓▓▓            ▓▓▓▓▓▓▓▓▓            
         ░▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▒▒▒▒▒░            
         ▒▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓░▓▓░░░            
          ░░░▓▓▓▓▓░▓▓▓░▓▓▓▓▓▓▓▓▓                  
                   ▓▓▓                            
                   ▓▓▓                            
                   ▓▓▓                            
                   ▓▓▓                            
                   ░░░                            
                                                  
                                                  
                                                  

View File

@ -0,0 +1,26 @@
                                                  
                                                  
                            ▓▓▓▓                  
             ░░░            ▓▓▓▓▓░▓               
             ▓▓▓            ▓▓▓▓▓▓▓               
             ▓▓▓            ▓▓▓▓▓▓▓               
             ▓▓▓            ▓▓▓▓▓▓▓               
             ▓▓▓▓▓▓         ▓▓▓▓                  
             ▓▓▓▓▓▓   ▓▓▓▒  ▓▓▓▓                  
             ▓▓▓▓▓▓   ▓▓▓▒  ▓▓▓▓                  
   ▓▓▓▒      ▓▓▓      ▓▓▓▒  ▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓      
   ▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓      
   ▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓      
   ▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓      ▓▓▓         
   ▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓      ▓▓▓         
                   ▓▓▓                ▓▓▓         
                   ▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓               
                   ▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓               
                   ▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓               
                   ▓▓▓                            
                   ▓▓▓                            
                ▓▓▓▓▓▓                            
                ▓▓▓▓▓▓                            
                ▓▓▓▓▓▓                            
                                                  


View File

@ -0,0 +1,26 @@
                         ▓▓▓▓                     
                         ▓▓▓▓                     
      ▓▓▓▓         ▓▓▓▓▓▓▓▓▓▓▓▓▓         ▓▓▓▓     
   ▓▓▓▓▓▓▓▓▓▓▓▓▓   ▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓     
   ▓▓▓▓▓▓▓▓▓▓▓▓▓   ▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓     
   ▓▓▓▓▓▓▓▓▓▓▓▓▓             ▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓░     
   ▓▓▓▓▓▓▓▓▓▓▓▓▓             ▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓░     
   ▓▓▓▓      ▓▓▓▓▓▓▓▓▓       ▓▓▓   ▓▓▓            
▓▓▓▓▓▓▓      ▓▓▓▓▓▓▓▓▓▓▓▓    ▓▓▓   ▓▓▓            
▓▓▓▓▓▓▓      ▓▓▓▓▓▓▓▓▓▓▓▓    ▓▓▓   ▓▓▓            
▓▓▓▓▓▓▓      ▓▓▓   ▓▓▓▓▓▓    ▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓  
   ▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓  
   ▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓  
   ▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓      ▓▓▓▓▓▓▓▓▓▓  
   ▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓      ▓▓▓▓▓▓▓▓▓▓  
   ▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓       ▓▓▓      ▓▓▓▓▓▓▓▓▓▓  
   ▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓   ▓▓▓   ▓▓▓▓  
   ▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓   ▓▓▓   ▓▓▓▓  
                   ▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓         ▓▓▓▓  
         ▓▓▓▓      ▓▓▓   ▓▓▓▓▓▓▓▓▓▓         ░▓▓▓  
         ▓▓▓▓      ▓▓▓   ▓▓▓▓▓▓▓▓▓▓         ░▓▓▓  
      ▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓   ▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓         
      ▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓          ▓▓▓▓▓▓▓▓▓         
      ▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓          ▓▓▓▓▓▓▓▓▓         
                   ▓▓▓