feat(ui): Better looking numbers

This commit is contained in:
Jakub Žitník 2025-04-02 22:05:02 +02:00
parent aa6a47fff9
commit 587cd938f6
Signed by: jzitnik
GPG Key ID: C577A802A6AF4EF3
21 changed files with 54 additions and 263 deletions

View File

@ -1,7 +1,6 @@
package cz.jzitnik.game;
import cz.jzitnik.game.sprites.*;
import cz.jzitnik.game.sprites.ui.Number;
import cz.jzitnik.tui.Sprite;
import cz.jzitnik.tui.SpriteList;
import lombok.extern.slf4j.Slf4j;
@ -82,7 +81,6 @@ public class SpriteLoader {
BREAKING,
HEART,
HUNGER,
NUMBER,
// Seeds
WHEAT,
@ -274,7 +272,6 @@ public class SpriteLoader {
SPRITES_MAP.put(SPRITES.BREAKING, new Breaking());
SPRITES_MAP.put(SPRITES.HEART, new Heart());
SPRITES_MAP.put(SPRITES.HUNGER, new Hunger());
SPRITES_MAP.put(SPRITES.NUMBER, new Number());
// SEEDS
SPRITES_MAP.put(SPRITES.WHEAT, new Farmable("wheat_stage1.ans", "wheat_stage2.ans", "wheat_stage3.ans"));

View File

@ -32,7 +32,7 @@ public class Chest implements RightClickHandler, Serializable {
int moveLeft = Math.max(0, (terminal.getWidth() / 2) - (widthPixels / 2));
List<String> sprites = game.getInventory().getSprites(items, spriteList, inventory.getSelectedItemInv() - 50);
List<String> sprites = game.getInventory().getSprites(items, spriteList, inventory.getSelectedItemInv() - 50, game);
for (int i = 0; i < ROW_AMOUNT; i++) {
for (int j = 0; j < CELL_HEIGHT; j++) {
@ -61,7 +61,7 @@ public class Chest implements RightClickHandler, Serializable {
size = buffer.toString().split("\n").length;
game.getInventory().renderFull(buffer, terminal, spriteList, false, Optional.of(size));
game.getInventory().renderFull(buffer, terminal, spriteList, false, Optional.of(size), game);
}
public void click(Game game, MouseEvent mouseEvent, Terminal terminal, ScreenRenderer screenRenderer) {

View File

@ -42,7 +42,7 @@ public class Furnace implements RightClickHandler, Serializable {
var inventory = game.getInventory();
int moveLeft = Math.max(0, (terminal.getWidth() / 2) - (widthPixels / 2));
List<String> sprites = game.getInventory().getSprites(items, spriteList, inventory.getSelectedItemInv() - 50);
List<String> sprites = game.getInventory().getSprites(items, spriteList, inventory.getSelectedItemInv() - 50, game);
String[] outputSprite = outputItem == null ? null
: SpriteCombiner.combineTwoSprites(
@ -50,7 +50,7 @@ public class Furnace implements RightClickHandler, Serializable {
? spriteList.getSprite(outputItem.getItem().getFirst().getSprite())
.getSprite(outputItem.getItem().getFirst().getSpriteState().get())
: spriteList.getSprite(outputItem.getItem().getFirst().getSprite()).getSprite(),
Numbers.getNumberSprite(outputItem.getAmount(), spriteList)).split("\n");
Numbers.getNumberSprite(outputItem.getAmount(), game)).split("\n");
for (int j = 0; j < CELL_HEIGHT; j++) {
buffer.append("\033[0m").append(" ".repeat(moveLeft));
@ -115,7 +115,7 @@ public class Furnace implements RightClickHandler, Serializable {
size = buffer.toString().split("\n").length;
game.getInventory().renderFull(buffer, terminal, spriteList, false, Optional.of(size));
game.getInventory().renderFull(buffer, terminal, spriteList, false, Optional.of(size), game);
}
public void click(Game game, MouseEvent mouseEvent, Terminal terminal, ScreenRenderer screenRenderer) {

View File

@ -36,9 +36,6 @@ public class Generation {
// Spawn player at a valid starting point
world[terrainHeight[256] - 1][256].add(steveBlock2);
world[terrainHeight[256] - 2][256].add(steveBlock);
game.getInventory().addItem(ItemBlockSupplier.getItem("sand"));
game.getInventory().addItem(ItemBlockSupplier.getItem("sand"));
}
private static void initializeWorld(List<Block>[][] world) {

View File

@ -41,7 +41,7 @@ public class Font {
stringBuilder.append("\033")
.append(line[x + 1].replaceAll("\\[(?!49m)[0-9;]+m", color).replaceAll("\\[49m", background));
}
stringBuilder.append("\033[0m\n");
stringBuilder.append("\n");
}
return stringBuilder.toString();
@ -104,6 +104,10 @@ public class Font {
}
public FontDTO scale(FontDTO font, int times) {
if (times <= 1) {
return font;
}
StringBuilder stringBuilder = new StringBuilder();
var line = font.getData();
var lines = line.split("\n");

View File

@ -1,67 +0,0 @@
package cz.jzitnik.game.sprites.ui;
import cz.jzitnik.tui.Sprite;
import lombok.extern.slf4j.Slf4j;
import java.util.HashMap;
import java.util.Optional;
@Slf4j
public class Number extends Sprite<Number.NumberState> {
public enum NumberState {
ZERO, ONE, TWO, THREE, FOUR, FIVE, SIX, SEVEN, EIGHT, NINE,
}
public Number() {
loadResources(new HashMap<>() {
{
put(NumberState.ZERO, "numbers/0.ans");
put(NumberState.ONE, "numbers/1.ans");
put(NumberState.TWO, "numbers/2.ans");
put(NumberState.THREE, "numbers/3.ans");
put(NumberState.FOUR, "numbers/4.ans");
put(NumberState.FIVE, "numbers/5.ans");
put(NumberState.SIX, "numbers/6.ans");
put(NumberState.SEVEN, "numbers/7.ans");
put(NumberState.EIGHT, "numbers/8.ans");
put(NumberState.NINE, "numbers/9.ans");
}
}, NumberState.class);
}
public static String fixForHotbar(String str, int place) {
var parts = str.split("\n");
// Make it 50x25
StringBuilder stringBuilder = new StringBuilder();
for (int i = 0; i < 24 - 12; i++) {
stringBuilder.append("\033[0m ".repeat(50)).append("\n");
}
for (int i = 0; i < 12; i++) {
stringBuilder.append("\033[0m ".repeat(50 - 12 * place)).append(parts[i])
.append("\033[0m ".repeat(12 * (place - 1))).append("\n");
}
stringBuilder.append("\033[0m ".repeat(50)).append("\n");
return stringBuilder.toString();
}
private String fix(String x) {
return x;
// return x.replaceAll("\033\\[38;5;1;48;5;16m", "\033[0m").replaceAll("\033\\[38;5;16;48;5;16m▓", "\033[0m ");
// TODO: Fix issue with sprite size decreasing
}
public String getSprite() {
return getSprite(NumberState.ZERO);
}
public String getSprite(NumberState key) {
log.debug("{}", key);
return fix(getResource(key));
}
@Override
public Optional<Class<NumberState>> getStates() {
return Optional.of(NumberState.class);
}
}

View File

@ -59,7 +59,7 @@ public class CraftingTable {
int moveLeft = Math.max(0, (terminal.getWidth() / 2) - (widthPixels / 2));
List<String> sprites = game.getInventory().getSprites(items, spriteList, inventory.getSelectedItemInv() - 50);
List<String> sprites = game.getInventory().getSprites(items, spriteList, inventory.getSelectedItemInv() - 50, game);
Optional<CraftingRecipe> recipe = CraftingRecipeList.getRecipe(Arrays.stream(items)
.map(item -> item == null ? null : item.getItem().getFirst().getId()).toArray(String[]::new));
@ -71,7 +71,7 @@ public class CraftingTable {
? spriteList.getSprite(inventoryItem.getItem().getFirst().getSprite())
.getSprite(inventoryItem.getItem().getFirst().getSpriteState().get())
: spriteList.getSprite(inventoryItem.getItem().getFirst().getSprite()).getSprite(),
Numbers.getNumberSprite(inventoryItem.getAmount(), spriteList)).split("\n")).orElse(null);
Numbers.getNumberSprite(inventoryItem.getAmount(), game)).split("\n")).orElse(null);
for (int i = 0; i < ROW_AMOUNT; i++) {
for (int j = 0; j < CELL_HEIGHT; j++) {
@ -114,7 +114,7 @@ public class CraftingTable {
size = buffer.toString().split("\n").length;
game.getInventory().renderFull(buffer, terminal, spriteList, false, Optional.of(size));
game.getInventory().renderFull(buffer, terminal, spriteList, false, Optional.of(size), game);
}
public void click(MouseEvent mouseEvent, Terminal terminal, ScreenRenderer screenRenderer) {

View File

@ -1,5 +1,6 @@
package cz.jzitnik.game.ui;
import cz.jzitnik.game.Game;
import cz.jzitnik.game.annotations.AutoTransient;
import cz.jzitnik.game.core.autotransient.AutoTransientSupport;
import cz.jzitnik.game.entities.items.InventoryItem;
@ -124,11 +125,11 @@ public class Inventory extends AutoTransientSupport {
return sprite.toString();
}
public void renderHotbar(StringBuilder buffer, SpriteList spriteList, Terminal terminal, boolean isFull) {
public void renderHotbar(StringBuilder buffer, SpriteList spriteList, Terminal terminal, boolean isFull, Game game) {
int termWidth = terminal.getWidth();
int startLeft = (termWidth / 2) - (INVENTORY_SIZE_PX / 2);
List<String> sprites = getSprites(hotbar, spriteList, (isFull ? selectedItemInv - 20 : itemInhHandIndex));
List<String> sprites = getSprites(hotbar, spriteList, (isFull ? selectedItemInv - 20 : itemInhHandIndex), game);
for (int i = 0; i < 26; i++) {
// Empty left space
@ -154,14 +155,14 @@ public class Inventory extends AutoTransientSupport {
}
public void renderFull(StringBuilder buffer, Terminal terminal, SpriteList spriteList, boolean includeCrafting,
Optional<Integer> moveTopCustom) {
Optional<Integer> moveTopCustom, Game game) {
int widthPixels = COLUMN_AMOUNT * (50 + 4) + 2;
int heightPixels = ROW_AMOUNT * (25 + 1);
int moveLeft = Math.max(0, (terminal.getWidth() / 2) - (widthPixels / 2));
int moveTop = moveTopCustom.orElse((terminal.getHeight() / 2) - (heightPixels / 2));
List<String> sprites = getSprites(items, spriteList, selectedItemInv);
List<String> sprites = getSprites(items, spriteList, selectedItemInv, game);
// Top center
buffer.append("\n".repeat(Math.max(0, moveTopCustom.isPresent() ? 0 : moveTop)));
@ -169,7 +170,7 @@ public class Inventory extends AutoTransientSupport {
buffer.append("\033[0m ".repeat(moveLeft));
buffer.append("\033[38;5;231;48;5;231m▓".repeat(widthPixels)).append("\033[0m\n");
String[] craftingTable = smallCraftingTable.render(spriteList).toString().split("\n");
String[] craftingTable = smallCraftingTable.render(spriteList, game).toString().split("\n");
int spacesFromTop = (ROW_AMOUNT - SmallCraftingTable.ROW_AMOUNT) / 2;
for (int i = 0; i < ROW_AMOUNT; i++) {
@ -213,10 +214,10 @@ public class Inventory extends AutoTransientSupport {
buffer.append("\n".repeat(10));
renderHotbar(buffer, spriteList, terminal, true);
renderHotbar(buffer, spriteList, terminal, true, game);
}
public List<String> getSprites(InventoryItem[] items, SpriteList spriteList, int selectedItem) {
public List<String> getSprites(InventoryItem[] items, SpriteList spriteList, int selectedItem, Game game) {
List<String> sprites = new ArrayList<>();
for (int i = 0; i < items.length; i++) {
@ -261,14 +262,14 @@ public class Inventory extends AutoTransientSupport {
.get())
: spriteList.getSprite(item.getItem().getFirst().getSprite())
.getSprite(),
Numbers.getNumberSprite(item.getAmount(), spriteList)));
Numbers.getNumberSprite(item.getAmount(), game)));
} else {
sprite = SpriteCombiner.combineTwoSprites(
item.getItem().getFirst().getSpriteState().isPresent()
? spriteList.getSprite(item.getItem().getFirst().getSprite())
.getSprite(item.getItem().getFirst().getSpriteState().get())
: spriteList.getSprite(item.getItem().getFirst().getSprite()).getSprite(),
Numbers.getNumberSprite(item.getAmount(), spriteList));
Numbers.getNumberSprite(item.getAmount(), game));
}
if (item.getItem().getFirst().getDurability() == 0

View File

@ -1,5 +1,6 @@
package cz.jzitnik.game.ui;
import cz.jzitnik.game.Game;
import cz.jzitnik.game.crafting.CraftingRecipe;
import cz.jzitnik.game.crafting.CraftingRecipeList;
import cz.jzitnik.game.entities.items.InventoryItem;
@ -43,10 +44,10 @@ public class SmallCraftingTable {
}
}
public StringBuilder render(SpriteList spriteList) {
public StringBuilder render(SpriteList spriteList, Game game) {
var buf = new StringBuilder();
List<String> sprites = inventory.getSprites(items, spriteList, inventory.getSelectedItemInv() - 29);
List<String> sprites = inventory.getSprites(items, spriteList, inventory.getSelectedItemInv() - 29, game);
Optional<CraftingRecipe> recipe = CraftingRecipeList.getRecipe(Arrays.stream(items)
.map(item -> item == null ? null : item.getItem().getFirst().getId()).toArray(String[]::new));
@ -58,7 +59,7 @@ public class SmallCraftingTable {
? spriteList.getSprite(inventoryItem.getItem().getFirst().getSprite())
.getSprite(inventoryItem.getItem().getFirst().getSpriteState().get())
: spriteList.getSprite(inventoryItem.getItem().getFirst().getSprite()).getSprite(),
Numbers.getNumberSprite(inventoryItem.getAmount(), spriteList)).split("\n")).orElse(null);
Numbers.getNumberSprite(inventoryItem.getAmount(), game)).split("\n")).orElse(null);
int counter = 0;
for (int i = 0; i < ROW_AMOUNT; i++) {

View File

@ -72,7 +72,7 @@ public class ScreenRenderer {
switch (game.getWindow()) {
// Different screens: Probably will need a rewrite
case INVENTORY -> game.getInventory().renderFull(main, terminal, spriteList, true, Optional.empty());
case INVENTORY -> game.getInventory().renderFull(main, terminal, spriteList, true, Optional.empty(), game);
case CRAFTING_TABLE -> game.getGameStates().craftingTable.render(main, terminal, spriteList);
case CHEST -> ((Chest) game.getWorld()[game.getGameStates().clickY][game.getGameStates().clickX].stream()
.filter(i -> i.getBlockId().equals("chest")).toList().getFirst().getData()).render(game, main,
@ -175,7 +175,7 @@ public class ScreenRenderer {
Healthbar.render(main, spriteList, terminal, game);
game.getInventory().renderHotbar(main, spriteList, terminal, false);
game.getInventory().renderHotbar(main, spriteList, terminal, false, game);
}
}
@ -203,5 +203,4 @@ public class ScreenRenderer {
return stringBuilder;
}
}

View File

@ -1,27 +1,30 @@
package cz.jzitnik.tui.utils;
import cz.jzitnik.game.SpriteLoader;
import cz.jzitnik.game.sprites.ui.Number;
import cz.jzitnik.tui.SpriteList;
import cz.jzitnik.game.Game;
public class Numbers {
private static Number.NumberState getNumberState(int digit) {
return switch (digit) {
case 0 -> Number.NumberState.ZERO;
case 1 -> Number.NumberState.ONE;
case 2 -> Number.NumberState.TWO;
case 3 -> Number.NumberState.THREE;
case 4 -> Number.NumberState.FOUR;
case 5 -> Number.NumberState.FIVE;
case 6 -> Number.NumberState.SIX;
case 7 -> Number.NumberState.SEVEN;
case 8 -> Number.NumberState.EIGHT;
case 9 -> Number.NumberState.NINE;
default -> throw new IllegalArgumentException("Unexpected number: " + digit);
};
public static String fixForHotbar(String str, int height, int width) {
var parts = str.split("\n");
// Make it 50x25
StringBuilder stringBuilder = new StringBuilder();
for (int i = 0; i < 24 - height; i++) {
stringBuilder.append("\033[0m ".repeat(50)).append("\n");
}
for (int i = 0; i < height; i++) {
stringBuilder.append("\033[0m ".repeat(48 - width));
stringBuilder.append(parts[i]);
stringBuilder.append("\033[49m ".repeat(2));
stringBuilder.append("\n");
}
stringBuilder.append("\033[0m ".repeat(50)).append("\n");
return stringBuilder.toString();
}
public static String getNumberSprite(int number, SpriteList spriteList) {
public static String getNumberSprite(int number, Game game) {
if (number == 1) {
StringBuilder sprite = new StringBuilder();
for (int i = 0; i < 25; i++) {
@ -31,24 +34,10 @@ public class Numbers {
return sprite.toString();
}
var nm = spriteList.getSprite(SpriteLoader.SPRITES.NUMBER);
if (number <= 9) {
Number.NumberState numberState = getNumberState(number);
var font = game.getGameStates().dependencies.font;
var text = font.getLine(String.valueOf(number), "[47m", "[49m");
var scaled = text;
return Number.fixForHotbar(nm.getSprite(numberState), 1);
}
String numStr = Integer.toString(number); // Convert to string
char firstChar = numStr.charAt(0); // Get first digit
char secondChar = numStr.charAt(1); // Get second digit
int firstDigit = Character.getNumericValue(firstChar);
int secondDigit = Character.getNumericValue(secondChar);
Number.NumberState numberState1 = getNumberState(firstDigit);
Number.NumberState numberState2 = getNumberState(secondDigit);
return SpriteCombiner.combineTwoSprites(Number.fixForHotbar(nm.getSprite(numberState1), 2),
Number.fixForHotbar(nm.getSprite(numberState2), 1));
return fixForHotbar(scaled.getData(), scaled.getHeight(), scaled.getWidth());
}
}

View File

@ -1,13 +0,0 @@
▓▓▓▓▓▓▓▓▓▓▓▓
▓▓▓▓▓▓▓▓▓▓▓▓
▓▓▓▓▓▓▓▓▓▓▓▓
▓▓▓▓▓▓▓▓▓▓▓▓
▓▓▓▓▓▓▓▓▓▓▓▓
▓▓▓▓▓▓▓▓▓▓▓▓
▓▓▓▓▓▓▓▓▓▓▓▓
▓▓▓▓▓▓▓▓▓▓▓▓
▓▓▓▓▓▓▓▓▓▓▓▓
▓▓▓▓▓▓▓▓▓▓▓▓
▓▓▓▓▓▓▓▓▓▓▓▓
▓▓▓▓▓▓▓▓▓▓▓▓


View File

@ -1,13 +0,0 @@
   ▒▓▓▓▒    
   ▒▓▓▓▒    
▓▓▓▓▓▓▓▒    
▓▓▓▓▓▓▓▒    
   ▒▓▓▓▒    
   ▒▓▓▓▒    
   ▒▓▓▓▒    
   ▒▓▓▓▒    
   ▒▓▓▓▒    
   ▒▓▓▓▒    
▓▓▓▓▓▓▓▓▓▓▓▓
▓▓▓▓▓▓▓▓▓▓▓▓


View File

@ -1,13 +0,0 @@
   ▒▓▓▓▒    
   ▒▓▓▓▒    
▓▓▓▒   ▒▓▓▓▓
▓▓▓▒   ▒▓▓▓▓
       ▒▓▓▓▓
       ▒▓▓▓▓
   ▒▓▓▓▒    
   ▒▓▓▓▒    
▓▓▓▒        
▓▓▓▒        
▓▓▓▓▓▓▓▓▓▓▓▓
▓▓▓▓▓▓▓▓▓▓▓▓


View File

@ -1,13 +0,0 @@
▓▓▓▓▓▓▓▒    
▓▓▓▓▓▓▓▒    
       ▒▓▓▓▓
       ▒▓▓▓▓
   ▒▓▓▓▒    
   ▒▓▓▓▒    
       ▒▓▓▓▓
       ▒▓▓▓▓
▓▓▓▒   ▒▓▓▓▓
▓▓▓▒   ▒▓▓▓▓
   ▒▓▓▓▒    
   ▒▓▓▓▒    


View File

@ -1,13 +0,0 @@
▓▓▓▓▓▓▓▓▓▓▓▓
▓▓▓▓▓▓▓▓▓▓▓▓
▓▓▓▓▓▓▓▓▓▓▓▓
▓▓▓▓▓▓▓▓▓▓▓▓
▓▓▓▓▓▓▓▓▓▓▓▓
▓▓▓▓▓▓▓▓▓▓▓▓
▓▓▓▓▓▓▓▓▓▓▓▓
▓▓▓▓▓▓▓▓▓▓▓▓
▓▓▓▓▓▓▓▓▓▓▓▓
▓▓▓▓▓▓▓▓▓▓▓▓
▓▓▓▓▓▓▓▓▓▓▓▓
▓▓▓▓▓▓▓▓▓▓▓▓


View File

@ -1,13 +0,0 @@
▓▓▓▓▓▓▓▓▓▓▓▓
▓▓▓▓▓▓▓▓▓▓▓▓
▓▓▓▓▓▓▓▓▓▓▓▓
▓▓▓▓▓▓▓▓▓▓▓▓
▓▓▓▓▓▓▓▓▓▓▓▓
▓▓▓▓▓▓▓▓▓▓▓▓
▓▓▓▓▓▓▓▓▓▓▓▓
▓▓▓▓▓▓▓▓▓▓▓▓
▓▓▓▓▓▓▓▓▓▓▓▓
▓▓▓▓▓▓▓▓▓▓▓▓
▓▓▓▓▓▓▓▓▓▓▓▓
▓▓▓▓▓▓▓▓▓▓▓▓


View File

@ -1,13 +0,0 @@
▓▓▓▓▓▓▓▓▓▓▓▓
▓▓▓▓▓▓▓▓▓▓▓▓
▓▓▓▓▓▓▓▓▓▓▓▓
▓▓▓▓▓▓▓▓▓▓▓▓
▓▓▓▓▓▓▓▓▓▓▓▓
▓▓▓▓▓▓▓▓▓▓▓▓
▓▓▓▓▓▓▓▓▓▓▓▓
▓▓▓▓▓▓▓▓▓▓▓▓
▓▓▓▓▓▓▓▓▓▓▓▓
▓▓▓▓▓▓▓▓▓▓▓▓
▓▓▓▓▓▓▓▓▓▓▓▓
▓▓▓▓▓▓▓▓▓▓▓▓


View File

@ -1,13 +0,0 @@
▓▓▓▓▓▓▓▓▓▓▓▓
▓▓▓▓▓▓▓▓▓▓▓▓
▓▓▓▓▓▓▓▓▓▓▓▓
▓▓▓▓▓▓▓▓▓▓▓▓
▓▓▓▓▓▓▓▓▓▓▓▓
▓▓▓▓▓▓▓▓▓▓▓▓
▓▓▓▓▓▓▓▓▓▓▓▓
▓▓▓▓▓▓▓▓▓▓▓▓
▓▓▓▓▓▓▓▓▓▓▓▓
▓▓▓▓▓▓▓▓▓▓▓▓
▓▓▓▓▓▓▓▓▓▓▓▓
▓▓▓▓▓▓▓▓▓▓▓▓


View File

@ -1,13 +0,0 @@
▓▓▓▓▓▓▓▓▓▓▓▓
▓▓▓▓▓▓▓▓▓▓▓▓
▓▓▓▓▓▓▓▓▓▓▓▓
▓▓▓▓▓▓▓▓▓▓▓▓
▓▓▓▓▓▓▓▓▓▓▓▓
▓▓▓▓▓▓▓▓▓▓▓▓
▓▓▓▓▓▓▓▓▓▓▓▓
▓▓▓▓▓▓▓▓▓▓▓▓
▓▓▓▓▓▓▓▓▓▓▓▓
▓▓▓▓▓▓▓▓▓▓▓▓
▓▓▓▓▓▓▓▓▓▓▓▓
▓▓▓▓▓▓▓▓▓▓▓▓


View File

@ -1,13 +0,0 @@
▓▓▓▓▓▓▓▓▓▓▓▓
▓▓▓▓▓▓▓▓▓▓▓▓
▓▓▓▓▓▓▓▓▓▓▓▓
▓▓▓▓▓▓▓▓▓▓▓▓
▓▓▓▓▓▓▓▓▓▓▓▓
▓▓▓▓▓▓▓▓▓▓▓▓
▓▓▓▓▓▓▓▓▓▓▓▓
▓▓▓▓▓▓▓▓▓▓▓▓
▓▓▓▓▓▓▓▓▓▓▓▓
▓▓▓▓▓▓▓▓▓▓▓▓
▓▓▓▓▓▓▓▓▓▓▓▓
▓▓▓▓▓▓▓▓▓▓▓▓