docs: Added some useless JavaDoc

This commit is contained in:
2025-05-27 08:06:14 +02:00
parent d09209848e
commit 418117c7fe
33 changed files with 805 additions and 22 deletions

View File

@ -14,6 +14,10 @@ import org.jline.terminal.TerminalBuilder;
import java.io.IOException;
/**
* The main class that starts the whole game. It initializes providers and sets
* up a terminal and creates the main game loop.
*/
@Slf4j
public class Main {
public static void main(String[] args) {

View File

@ -25,6 +25,7 @@ import cz.jzitnik.game.ui.Inventory;
import cz.jzitnik.game.handlers.rightclick.RightClickHandlerProvider;
import cz.jzitnik.tui.ScreenMovingCalculationProvider;
import cz.jzitnik.tui.ScreenRenderer;
import cz.jzitnik.game.stats.Stats;
import lombok.Getter;
import lombok.Setter;
@ -54,6 +55,7 @@ public class Game {
private final Inventory inventory = new Inventory();
private transient EntitySpawnProvider entitySpawnProvider = new EntitySpawnProvider();
private transient GameStates gameStates = new GameStates(this);
private Stats stats = new Stats();
/** Current time of day in the game (0600 range). */
@Setter
@ -114,6 +116,8 @@ public class Game {
world[cords[1] - 1][cords[0]].remove(player.getPlayerBlock1());
screenRenderer.render(this);
stats.setBlocksTraveled(stats.getBlocksTraveled() + 1);
entitySpawnProvider.update(this, terminal);
playMovePlayerSound(cords[0] + 1, cords[1]);
@ -143,6 +147,8 @@ public class Game {
world[cords[1] - 1][cords[0]].remove(player.getPlayerBlock1());
screenRenderer.render(this);
stats.setBlocksTraveled(stats.getBlocksTraveled() + 1);
entitySpawnProvider.update(this, terminal);
playMovePlayerSound(cords[0] - 1, cords[1]);
@ -170,6 +176,8 @@ public class Game {
world[cords[1] - 2][cords[0]].add(player.getPlayerBlock1());
world[cords[1]][cords[0]].remove(player.getPlayerBlock2());
stats.setBlocksTraveled(stats.getBlocksTraveled() + 1);
new Thread(() -> {
try {
Thread.sleep(400);
@ -369,6 +377,8 @@ public class Game {
}
}
stats.setBlocksMined(stats.getBlocksMined() + 1);
screenRenderer.render(this);
update(screenRenderer);
@ -460,6 +470,8 @@ public class Game {
world[cords2[1]][cords2[0]].remove(player.getPlayerBlock2());
player.addFalling();
stats.setBlocksTraveled(stats.getBlocksTraveled() + 1);
screenRenderer.render(this);
} else {
ArrayList<Block> combinedList = new ArrayList<>();
@ -560,6 +572,7 @@ public class Game {
}
}
gameStates.dependencies.eventHandlerProvider.handlePlace(screenRenderer, this, x, y);
stats.setBlocksPlaced(stats.getBlocksPlaced() + 1);
screenRenderer.render(this);
}
}

View File

@ -7,6 +7,9 @@ import lombok.extern.slf4j.Slf4j;
import java.io.*;
/**
* Handles saving and loading the game state using Kryo serialization.
*/
@Slf4j
public class GameSaver {
@ -14,12 +17,20 @@ public class GameSaver {
private final Kryo kryo;
/**
* Initializes the GameSaver with Kryo serialization setup.
*/
public GameSaver() {
this.kryo = new Kryo();
kryo.setRegistrationRequired(false);
kryo.setReferences(true);
}
/**
* Saves the current game state to a file.
*
* @param game the game instance to be saved
*/
public void save(Game game) {
try (Output output = new Output(new FileOutputStream(SAVE_FILE))) {
kryo.writeClassAndObject(output, game);
@ -28,6 +39,12 @@ public class GameSaver {
}
}
/**
* Loads the game state from a file. If the save file does not exist,
* a new game instance is returned.
*
* @return the loaded game instance or a new game if no save is found
*/
public Game load() {
log.info("Loading game");

View File

@ -6,7 +6,6 @@ import cz.jzitnik.game.annotations.ItemRegistry;
import cz.jzitnik.game.entities.items.Item;
import cz.jzitnik.game.entities.items.ItemType;
import cz.jzitnik.game.sprites.Dye;
import cz.jzitnik.game.sprites.WoolItem;
@Fuel(0.5)
@ItemRegistry("black_dye")

View File

@ -0,0 +1,11 @@
package cz.jzitnik.game.stats;
import lombok.Data;
@Data
public class Stats {
private int secondsPlayed = 0;
private int blocksPlaced = 0;
private int blocksMined = 0;
private int blocksTraveled = 0;
}

View File

@ -0,0 +1,21 @@
package cz.jzitnik.game.stats;
import cz.jzitnik.game.annotations.ThreadRegistry;
@ThreadRegistry
public class StatsThread extends Thread {
private Stats stats;
@Override
public void run() {
try {
stats.setSecondsPlayed(stats.getSecondsPlayed());
while (true) {
Thread.sleep(1000);
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}

View File

@ -7,12 +7,16 @@ import java.util.Set;
import cz.jzitnik.game.Game;
import cz.jzitnik.game.annotations.ThreadRegistry;
import cz.jzitnik.game.entities.Player;
import cz.jzitnik.game.stats.Stats;
import cz.jzitnik.tui.ScreenRenderer;
import lombok.extern.slf4j.Slf4j;
import org.jline.terminal.Terminal;
import org.reflections.Reflections;
/**
* Scans and registers all threads annotated with {@link ThreadRegistry} and starts them when requested.
*/
@Slf4j
public class ThreadProvider {
private final Game game;
@ -21,6 +25,9 @@ public class ThreadProvider {
private final boolean[] isRunning;
private final List<Thread> list = new ArrayList<>();
/**
* Starts all registered threads.
*/
public void start() {
log.info("Loading all the threads");
for (Thread thread : list) {
@ -28,6 +35,14 @@ public class ThreadProvider {
}
}
/**
* Constructs a ThreadProvider and registers all eligible threads.
*
* @param game The game instance.
* @param screenRenderer The screen renderer.
* @param terminal The terminal.
* @param isRunning A shared boolean array for controlling thread execution.
*/
public ThreadProvider(Game game, ScreenRenderer screenRenderer, Terminal terminal, boolean[] isRunning) {
this.game = game;
this.screenRenderer = screenRenderer;
@ -36,6 +51,10 @@ public class ThreadProvider {
registerHandlers();
}
/**
* Scans the package for classes annotated with {@link ThreadRegistry} and instantiates valid threads.
* Matches constructors using known parameter types (e.g., Game, Stats, Terminal, etc.).
*/
private void registerHandlers() {
Reflections reflections = new Reflections("cz.jzitnik.game.threads");
Set<Class<?>> handlerClasses = reflections.getTypesAnnotatedWith(ThreadRegistry.class);
@ -52,6 +71,8 @@ public class ThreadProvider {
Class<?> type = paramTypes[i];
if (type == Game.class)
params[i] = game;
if (type == Stats.class)
params[i] = game.getStats();
else if (type == ScreenRenderer.class)
params[i] = screenRenderer;
else if (type == Terminal.class)

View File

@ -4,11 +4,19 @@ import cz.jzitnik.game.annotations.ThreadRegistry;
import cz.jzitnik.game.entities.Player;
import lombok.AllArgsConstructor;
/**
* A thread responsible for periodically regenerating the player's health.
* Executes every 4 seconds and stops if the thread is interrupted.
*/
@AllArgsConstructor
@ThreadRegistry
public class HealthRegenerationThread extends Thread {
private final Player player;
/**
* Runs the health regeneration loop, healing the player every 4 seconds.
* The loop continues until the thread is interrupted.
*/
@Override
public void run() {
while (true) {

View File

@ -4,6 +4,10 @@ import cz.jzitnik.game.annotations.ThreadRegistry;
import cz.jzitnik.game.entities.Player;
import lombok.AllArgsConstructor;
/**
* A thread that periodically reduces the player's hunger level.
* Runs continuously, sleeping between hunger drain cycles.
*/
@AllArgsConstructor
@ThreadRegistry
public class HungerDrainThread extends Thread {

View File

@ -13,6 +13,10 @@ import java.nio.BufferUnderflowException;
import java.util.Optional;
import cz.jzitnik.game.annotations.ThreadRegistry;
/**
* A thread that handles keyboard and mouse input from the terminal.
* Responsible for routing input events to the appropriate handlers based on the current window.
*/
@ThreadRegistry
public class InputHandlerThread extends Thread {
private final Game game;
@ -20,6 +24,13 @@ public class InputHandlerThread extends Thread {
private final ScreenRenderer screenRenderer;
private final MouseHandler mouseHandler;
/**
* Constructs the input handler thread with required dependencies.
*
* @param game The game instance.
* @param terminal The terminal to read input from.
* @param screenRenderer Renderer to update the game screen.
*/
public InputHandlerThread(Game game, Terminal terminal, ScreenRenderer screenRenderer) {
this.game = game;
this.terminal = terminal;
@ -27,6 +38,9 @@ public class InputHandlerThread extends Thread {
this.mouseHandler = new MouseHandler(game, terminal, screenRenderer);
}
/**
* Thread run method that listens for keyboard and mouse events and dispatches them appropriately.
*/
@Override
public void run() {
try {

View File

@ -6,6 +6,10 @@ import cz.jzitnik.game.entities.Player;
import cz.jzitnik.tui.ScreenRenderer;
import lombok.AllArgsConstructor;
/**
* A thread that periodically checks the player's hunger level.
* If hunger reaches 0, it applies damage to the player.
*/
@AllArgsConstructor
@ThreadRegistry
public class NoHungerThread extends Thread {

View File

@ -4,12 +4,23 @@ import cz.jzitnik.game.Game;
import java.util.HashMap;
/**
* Handles clean-up or exit operations when closing certain UI windows.
*/
public class CloseHandler {
/**
* A functional interface for handling window close operations on the game instance.
*
* @param <T> The type of the argument passed to the function.
*/
@FunctionalInterface
public interface Function<T> {
void call(T t);
}
/**
* A map of window types to their corresponding close handler functions.
*/
public static HashMap<Window, Function<Game>> handles = new HashMap<>();
static {
@ -17,6 +28,12 @@ public class CloseHandler {
handles.put(Window.INVENTORY, game -> game.getInventory().exit());
}
/**
* Executes the close handler function for the specified window, if one is registered.
*
* @param window The window being closed.
* @param game The game instance.
*/
public static void handle(Window window, Game game) {
if (handles.containsKey(window)) {
var func = handles.get(window);

View File

@ -15,6 +15,10 @@ import java.util.Arrays;
import java.util.List;
import java.util.Optional;
/**
* Represents the crafting table UI where the player can combine items
* according to recipes to create new ones.
*/
public class CraftingTable {
private final Game game;
private static final int ROW_AMOUNT = 3;
@ -26,10 +30,20 @@ public class CraftingTable {
private final InventoryItem[] items = new InventoryItem[ROW_AMOUNT * COLUMN_AMOUNT];
private int size;
/**
* Sets the current game window to the crafting table view.
*
* @param ignored Ignored parameter for compatibility.
* @param ignored2 Ignored parameter for compatibility.
*/
public void render(int ignored, int ignored2) {
game.setWindow(Window.CRAFTING_TABLE);
}
/**
* Picks up the result of a valid recipe if one exists in the crafting grid,
* adds it to the inventory, and decreases the input items accordingly.
*/
public void pickup() {
Optional<CraftingRecipe> recipe = CraftingRecipeList.getRecipe(Arrays.stream(items)
.map(item -> item == null ? null : item.getItem().getFirst().getId()).toArray(String[]::new));
@ -53,6 +67,13 @@ public class CraftingTable {
}
}
/**
* Renders the crafting table UI and the inventory below it.
*
* @param buffer Buffer to append rendered content to.
* @param terminal Terminal to determine screen dimensions.
* @param spriteList Sprite list to fetch item visuals.
*/
public void render(StringBuilder buffer, Terminal terminal, SpriteList spriteList) {
int widthPixels = COLUMN_AMOUNT * (CELL_WIDTH + BORDER_SIZE) + BORDER_SIZE;
var inventory = game.getInventory();
@ -117,6 +138,13 @@ public class CraftingTable {
game.getInventory().renderFull(buffer, terminal, spriteList, false, Optional.of(size), game);
}
/**
* Handles mouse clicks inside the crafting grid or on the output slot.
*
* @param mouseEvent Mouse event with coordinates and type.
* @param terminal Terminal for dimension reference.
* @param screenRenderer Renderer to trigger screen updates.
*/
public void click(MouseEvent mouseEvent, Terminal terminal, ScreenRenderer screenRenderer) {
int x = mouseEvent.getX();
int y = mouseEvent.getY();
@ -154,6 +182,10 @@ public class CraftingTable {
Optional.of(items));
}
/**
* Transfers all remaining items in the crafting grid back to the inventory
* and clears the crafting grid.
*/
public void exit() {
// Put all items from crafting to inv
for (int i = 0; i < items.length; i++) {
@ -167,6 +199,11 @@ public class CraftingTable {
}
}
/**
* Constructor for the crafting table.
*
* @param game Reference to the game instance.
*/
public CraftingTable(Game game) {
this.game = game;
}

View File

@ -12,6 +12,10 @@ import cz.jzitnik.game.sprites.ui.Font.Size;
import cz.jzitnik.tui.ScreenRenderer;
import lombok.extern.slf4j.Slf4j;
/**
* Displays the death screen UI with a "You Died" message and a respawn button.
* Handles rendering and mouse interactions for the respawn action.
*/
@Slf4j
public class DeathScreen {
private int btnWidth;
@ -21,6 +25,13 @@ public class DeathScreen {
private int textButtonMargin;
private boolean buttonHover;
/**
* Renders the death screen with a heading and the respawn button.
*
* @param buffer The rendering buffer.
* @param terminal The terminal to determine size and dimensions.
* @param game The game instance for accessing dependencies.
*/
public void render(StringBuilder buffer, Terminal terminal, Game game) {
log.debug("Rendering death screen");
var font = game.getGameStates().dependencies.font;
@ -37,6 +48,16 @@ public class DeathScreen {
renderButton(buffer, "Respawn", width, font, terminal, buttonHover);
}
/**
* Renders a single button with hover state and styling.
*
* @param buffer The rendering buffer.
* @param txt The text to display in the button.
* @param width The terminal width for calculating centering.
* @param font The font renderer.
* @param terminal The terminal object.
* @param selected Whether the button is currently hovered.
*/
public void renderButton(StringBuilder buffer, String txt, int width, Font font, Terminal terminal,
boolean selected) {
btnWidth = Math.min(350, (int) (width * (3.0 / 4)));
@ -62,6 +83,15 @@ public class DeathScreen {
}
}
/**
* Handles mouse interactions for the death screen, including hover and click
* detection on the "Respawn" button.
*
* @param mouseEvent The mouse event that occurred.
* @param terminal The terminal used for dimensions.
* @param screenRenderer The screen renderer to trigger re-renders.
* @param game The game instance to change state upon respawn.
*/
public void handleMouse(MouseEvent mouseEvent, Terminal terminal, ScreenRenderer screenRenderer, Game game) {
int x = mouseEvent.getX();
int y = mouseEvent.getY();

View File

@ -7,28 +7,59 @@ import cz.jzitnik.game.sprites.ui.Font.*;
import cz.jzitnik.tui.ScreenRenderer;
import cz.jzitnik.tui.utils.Menu;
/**
* Handles the in-game escape menu functionality, including rendering options,
* handling mouse input, and managing save and exit logic.
*/
public class Escape {
private Game game;
private Menu menu;
/**
* Constructs the Escape menu for the given game instance.
*
* @param game The current game instance.
*/
public Escape(Game game) {
this.game = game;
this.menu = new Menu(game, "2DCraft", new String[] { "Continue", "Options", "Save and exit" },
this::onButtonClick);
}
/**
* Resets the escape menu to its initial state.
*/
public void reset() {
menu.reset();
}
/**
* Renders the escape menu onto the screen.
*
* @param buffer The buffer to which rendering output is appended.
* @param terminal The terminal used to get display properties.
*/
public void render(StringBuilder buffer, Terminal terminal) {
menu.render(buffer, terminal);
}
/**
* Handles mouse input events within the escape menu.
*
* @param mouseEvent The mouse event triggered by the user.
* @param terminal The terminal used for input/output.
* @param screenRenderer The screen renderer for drawing changes.
*/
public void mouse(MouseEvent mouseEvent, Terminal terminal, ScreenRenderer screenRenderer) {
menu.handleMouse(mouseEvent, terminal, screenRenderer);
}
/**
* Renders a "Saving" screen with centered large-font text.
*
* @param buffer The buffer to which rendering output is appended.
* @param terminal The terminal used to calculate screen size.
*/
private void onButtonClick(int index, ScreenRenderer screenRenderer) {
switch (index) {
case 0:
@ -49,6 +80,12 @@ public class Escape {
screenRenderer.render(game);
}
/**
* Renders a "Saved" screen with centered large-font text.
*
* @param buffer The buffer to which rendering output is appended.
* @param terminal The terminal used to calculate screen size.
*/
public void renderSave(StringBuilder buffer, Terminal terminal) {
var font = game.getGameStates().dependencies.font;
var savingText = font.line(terminal, "Saving", Size.LARGE, Align.CENTER);
@ -59,6 +96,12 @@ public class Escape {
buffer.append(savingText.getData());
}
/**
* Callback for when a button in the escape menu is clicked.
*
* @param index The index of the clicked button (0 = Continue, 1 = Options, 2 = Save and exit).
* @param screenRenderer The screen renderer used to refresh the display.
*/
public void renderSaved(StringBuilder buffer, Terminal terminal) {
var font = game.getGameStates().dependencies.font;
var savingText = font.line(terminal, "Saved", Size.LARGE, Align.CENTER);

View File

@ -9,7 +9,24 @@ import org.jline.terminal.Terminal;
import static cz.jzitnik.game.ui.Inventory.INVENTORY_SIZE_PX;
/**
* A utility class for rendering the player's health and hunger bars.
* The bars are displayed in a graphical form using text sprites.
*/
public class Healthbar {
/**
* Renders the health and hunger bar of the player using sprite graphics.
*
* <p>This method uses the terminal width to position the bars, and pulls sprite data from the
* provided {@link SpriteList} to render the correct number of hearts and hunger icons
* based on the player's current health and hunger levels.</p>
*
* @param buffer The StringBuilder to which the rendering output is appended.
* @param spriteList The list of sprites used for rendering the hearts and hunger icons.
* @param terminal The terminal from which screen width is obtained.
* @param game The current game instance, from which player state is read.
*/
public static void render(StringBuilder buffer, SpriteList spriteList, Terminal terminal, Game game) {
int termWidth = terminal.getWidth();
int startLeft = Math.max(0, (termWidth / 2) - (INVENTORY_SIZE_PX / 2));

View File

@ -1,5 +0,0 @@
package cz.jzitnik.game.ui;
public class HomeScreen {
}

View File

@ -14,6 +14,9 @@ import java.util.ArrayList;
import java.util.List;
import java.util.Optional;
/**
* Represents a player's inventory including hotbar, crafting table, and item management.
*/
@Getter
public class Inventory {
public static final int INVENTORY_SIZE_PX = 470;
@ -32,6 +35,11 @@ public class Inventory {
@Setter
private boolean rightClick = false;
/**
* Returns the currently held item in the hotbar's selected hand index.
*
* @return Optional containing the held Item if present, otherwise empty.
*/
public Optional<Item> getItemInHand() {
if (hotbar[itemInhHandIndex] == null) {
return Optional.empty();
@ -39,6 +47,9 @@ public class Inventory {
return Optional.of(hotbar[itemInhHandIndex].getItem().getLast());
}
/**
* Decreases the amount of the item in hand by one, removing it if the amount reaches zero.
*/
public void decreaseItemInHand() {
if (hotbar[itemInhHandIndex] == null) {
return;
@ -52,6 +63,13 @@ public class Inventory {
hotbar[itemInhHandIndex].decrease();
}
/**
* Attempts to place a new item into the inventory or hotbar.
* If inventory and hotbar are full, the item is lost.
*
* @param item The item to place.
*/
private void placeItem(Item item) {
var inventoryItem = new InventoryItem(item);
@ -74,18 +92,33 @@ public class Inventory {
// If inventory is full the item is lost
}
/**
* Adds an InventoryItem's amount of items into the inventory.
*
* @param item InventoryItem to add.
*/
public void addItem(InventoryItem item) {
for (int i = 0; i < item.getAmount(); i++) {
addItem(item.getItem().get(i));
}
}
/**
* Adds a list of Items to the inventory.
*
* @param item List of Items to add.
*/
public void addItem(List<Item> item) {
for (Item i : item) {
addItem(i);
}
}
/**
* Adds a single Item to the inventory, stacking if possible.
*
* @param item Item to add.
*/
public void addItem(Item item) {
if (!item.isStackable()) {
placeItem(item);
@ -113,6 +146,11 @@ public class Inventory {
placeItem(item);
}
/**
* Returns a string representing the background for the hotbar slots.
*
* @return Background sprite string.
*/
private String getHotbarBackground() {
StringBuilder sprite = new StringBuilder();
for (int i = 0; i < 25; i++) {
@ -122,6 +160,15 @@ public class Inventory {
return sprite.toString();
}
/**
* Renders the hotbar UI onto the given buffer.
*
* @param buffer StringBuilder buffer to append rendering output.
* @param spriteList SpriteList used for rendering item sprites.
* @param terminal Terminal object for screen dimensions.
* @param isFull Whether full inventory mode is active.
* @param game Current game instance.
*/
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);
@ -151,8 +198,18 @@ public class Inventory {
}
}
/**
* Renders the full inventory UI including items and crafting table.
*
* @param buffer StringBuilder buffer to append rendering output.
* @param terminal Terminal object for screen dimensions.
* @param spriteList SpriteList used for rendering item sprites.
* @param includeCrafting Whether to include the crafting UI.
* @param moveTopCustom Optional vertical offset.
* @param game Current game instance.
*/
public void renderFull(StringBuilder buffer, Terminal terminal, SpriteList spriteList, boolean includeCrafting,
Optional<Integer> moveTopCustom, Game game) {
Optional<Integer> moveTopCustom, Game game) {
int widthPixels = COLUMN_AMOUNT * (50 + 4) + 2;
int heightPixels = ROW_AMOUNT * (25 + 1);
@ -214,6 +271,14 @@ public class Inventory {
renderHotbar(buffer, spriteList, terminal, true, game);
}
/**
* Generates a list of sprite strings for the given inventory items, highlighting selected.
* @param items Array of InventoryItem objects.
* @param spriteList SpriteList used to retrieve sprites.
* @param selectedItem Index of the selected item.
* @param game Current game instance.
* @return List of sprite strings representing each inventory slot.
*/
public List<String> getSprites(InventoryItem[] items, SpriteList spriteList, int selectedItem, Game game) {
List<String> sprites = new ArrayList<>();
@ -255,16 +320,16 @@ public class Inventory {
SpriteCombiner.combineTwoSprites(
item.getItem().getFirst().getSpriteState().isPresent()
? spriteList.getSprite(item.getItem().getFirst().getSprite())
.getSprite(item.getItem().getFirst().getSpriteState()
.get())
.getSprite(item.getItem().getFirst().getSpriteState()
.get())
: spriteList.getSprite(item.getItem().getFirst().getSprite())
.getSprite(),
.getSprite(),
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())
.getSprite(item.getItem().getFirst().getSpriteState().get())
: spriteList.getSprite(item.getItem().getFirst().getSprite()).getSprite(),
Numbers.getNumberSprite(item.getAmount(), game));
}
@ -280,6 +345,12 @@ public class Inventory {
return sprites;
}
/**
* Retrieves an InventoryDTO wrapping the item array and index for the given absolute index.
* @param index Index of the item in inventory/hotbar/crafting table.
* @param i Optional custom InventoryItem array for special inventories.
* @return InventoryDTO representing the selected inventory slot.
*/
public InventoryDTO getItem(int index, Optional<InventoryItem[]> i) {
if (index < 20) {
// Normal inventory
@ -295,6 +366,11 @@ public class Inventory {
}
}
/**
* Returns the InventoryItem at the selected index without modifying inventory.
* @param i Optional custom InventoryItem array.
* @return InventoryItem at the selected index or null if none.
*/
public InventoryItem getSelectedItemNo(Optional<InventoryItem[]> i) {
InventoryDTO data = getItem(selectedItemInv, i);
if (selectedItemInv == -1) {

View File

@ -10,7 +10,21 @@ import java.util.Optional;
import static cz.jzitnik.game.ui.Inventory.*;
/**
* Handles mouse click events on the inventory UI, dispatching to appropriate
* handlers for crafting table, hotbar, and main inventory.
*/
public class InventoryClickHandler {
/**
* Processes a mouse click event within the inventory UI.
*
* @param mouseEvent The mouse event to handle.
* @param terminal Terminal instance for dimension calculations.
* @param screenRenderer Renderer to update the screen after changes.
* @param game Current game instance.
* @param moveTopCustom Optional custom top offset.
* @param i Optional inventory items array for manipulation.
*/
public static void click(MouseEvent mouseEvent, Terminal terminal, ScreenRenderer screenRenderer, Game game,
Optional<Integer> moveTopCustom, Optional<InventoryItem[]> i) {
if (mouseEvent.getType() != MouseEvent.Type.Pressed)
@ -98,6 +112,17 @@ public class InventoryClickHandler {
screenRenderer.render(game);
}
/**
* Handles item clicks with left and right mouse button logic for picking up,
* placing, and merging items.
*
* @param mouseEvent The mouse event triggered by the user.
* @param inventory The inventory instance to modify.
* @param items The item array being manipulated.
* @param index The index of the clicked item.
* @param offset The offset to apply to the index.
* @param i Optional inventory items array for manipulation.
*/
public static void handleItemClick(MouseEvent mouseEvent, Inventory inventory, Object[] items, int index,
int offset, Optional<InventoryItem[]> i) {
int actualIndex = index + offset;

View File

@ -4,6 +4,9 @@ import cz.jzitnik.game.entities.items.InventoryItem;
import lombok.AllArgsConstructor;
import lombok.Getter;
/**
* Data Transfer Object for Inventory containing items and a selected index.
*/
@AllArgsConstructor
@Getter
public class InventoryDTO {

View File

@ -7,17 +7,34 @@ import cz.jzitnik.game.sprites.ui.Font.*;
import cz.jzitnik.tui.ScreenRenderer;
import lombok.extern.slf4j.Slf4j;
/**
* Represents the options menu where the user can configure settings
* such as sound volume using a slider UI.
*/
@Slf4j
public class Options {
private Game game;
private int top;
private int sliderWidth;
private int sliderLeftpad;
private int sliderLeftpad;
/**
* Creates an Options menu for the given game instance.
*
* @param game the game instance providing configuration and resources
*/
public Options(Game game) {
this.game = game;
}
/**
* Renders the options menu UI including the heading, labels,
* and the volume slider.
*
* @param buffer the string builder to append the rendered UI
* @param terminal the terminal context for size and rendering info
*/
public void render(StringBuilder buffer, Terminal terminal) {
var buf = new StringBuilder();
var font = game.getGameStates().dependencies.font;
@ -40,6 +57,13 @@ public class Options {
buffer.append(buf);
}
/**
* Renders the slider bar indicating the current volume percentage.
*
* @param buffer the string builder to append the slider UI
* @param terminal the terminal context for width and rendering
* @param percentage the current volume percentage to display
*/
private void renderSlider(StringBuilder buffer, Terminal terminal, int percentage) {
var defaultColor = "\033[48;2;116;115;113m";
var fullColor = "\033[48;2;70;70;70m";
@ -63,6 +87,14 @@ public class Options {
}
}
/**
* Handles mouse input events to update the sound volume based on
* slider interaction.
*
* @param mouseEvent the mouse event containing position and type
* @param terminal the terminal context for coordinate mapping
* @param screenRenderer the renderer to update the screen after changes
*/
public void handleMouse(MouseEvent mouseEvent, Terminal terminal, ScreenRenderer screenRenderer) {
int x = mouseEvent.getX();
int y = mouseEvent.getY() - top;

View File

@ -13,14 +13,30 @@ import java.util.Arrays;
import java.util.List;
import java.util.Optional;
/**
* Represents a small 2x2 crafting table that allows crafting of items
* based on defined recipes.
*/
public class SmallCraftingTable {
/** Number of rows in the crafting grid. */
public static final int ROW_AMOUNT = 2;
/** Number of columns in the crafting grid. */
public static final int COLUMN_AMOUNT = 2;
/** The inventory associated with the crafting table. */
private Inventory inventory;
/** The items currently placed in the crafting grid. */
@Getter
private InventoryItem[] items = new InventoryItem[4];
/**
* Attempts to craft an item based on the items in the crafting grid.
* If a matching recipe is found, the crafted item is added to the inventory,
* and input items are consumed.
*/
public void pickup() {
Optional<CraftingRecipe> recipe = CraftingRecipeList.getRecipe(Arrays.stream(items)
.map(item -> item == null ? null : item.getItem().getFirst().getId()).toArray(String[]::new));
@ -44,6 +60,13 @@ public class SmallCraftingTable {
}
}
/**
* Renders the 2x2 crafting grid along with the result preview sprite.
*
* @param spriteList the list of available sprites
* @param game the current game context
* @return a string builder containing the ANSI-rendered crafting table
*/
public StringBuilder render(SpriteList spriteList, Game game) {
var buf = new StringBuilder();
@ -105,6 +128,11 @@ public class SmallCraftingTable {
return buf;
}
/**
* Creates a small crafting table associated with the given inventory.
*
* @param inventory the inventory to use for storing crafted items
*/
public SmallCraftingTable(Inventory inventory) {
this.inventory = inventory;
}

View File

@ -1,5 +1,56 @@
package cz.jzitnik.game.ui;
/**
* Represents the different UI windows/screens available in the game.
*/
public enum Window {
WORLD, INVENTORY, CRAFTING_TABLE, CHEST, FURNACE, ESC, SAVE_EXIT, SAVED, OPTIONS, DEATH_SCREEN,
/**
* Main game world view where the player moves and interacts.
*/
WORLD,
/**
* Player's inventory screen.
*/
INVENTORY,
/**
* Crafting table interface for combining items.
*/
CRAFTING_TABLE,
/**
* Chest interface for item storage.
*/
CHEST,
/**
* Furnace interface for smelting items.
*/
FURNACE,
/**
* Escape/pause menu.
*/
ESC,
/**
* Save and exit confirmation screen.
*/
SAVE_EXIT,
/**
* Screen indicating the game has been successfully saved.
*/
SAVED,
/**
* Options or settings menu.
*/
OPTIONS,
/**
* Screen displayed when the player dies.
*/
DEATH_SCREEN,
}

View File

@ -10,12 +10,21 @@ import java.util.ArrayList;
import java.util.List;
import java.util.Optional;
/**
* Handles mouse interactions in the terminal-based UI.
* Supports clicking, scrolling, and moving to interact with blocks, inventory, and UI components.
*/
@AllArgsConstructor
public class MouseHandler {
private Game game;
private Terminal terminal;
private ScreenRenderer screenRenderer;
/**
* Handles a generic mouse event by dispatching to the correct handler based on the event type.
*
* @param mouseEvent The mouse event received from the terminal.
*/
public void handle(MouseEvent mouseEvent) {
if (mouseEvent.getButton() == MouseEvent.Button.Button1 && mouseEvent.getType() == MouseEvent.Type.Pressed) {
leftClick(mouseEvent);
@ -37,6 +46,11 @@ public class MouseHandler {
}
}
/**
* Handles mouse scroll events to change the selected hotbar slot when in the world view.
*
* @param mouseEvent The mouse scroll event.
*/
private void scroll(MouseEvent mouseEvent) {
if (game.getWindow() == Window.WORLD) {
if (mouseEvent.getButton() == MouseEvent.Button.WheelUp) {
@ -57,6 +71,12 @@ public class MouseHandler {
}
}
/**
* Handles left-click actions on the screen.
* Attempts to hit or mine the block the cursor is over, depending on the game state.
*
* @param mouseEvent The mouse click event (left button).
*/
private void leftClick(MouseEvent mouseEvent) {
int mouseX = mouseEvent.getX();
int mouseY = mouseEvent.getY();
@ -87,6 +107,12 @@ public class MouseHandler {
}
}
/**
* Handles right-click actions on the screen.
* Attempts to place a block at the cursor's current location.
*
* @param mouseEvent The mouse click event (right button).
*/
private void rightClick(MouseEvent mouseEvent) {
int mouseX = mouseEvent.getX();
int mouseY = mouseEvent.getY();
@ -110,6 +136,12 @@ public class MouseHandler {
game.build(blockX, blockY, screenRenderer);
}
/**
* Handles mouse movement events.
* Used for highlighting mineable blocks in the world view when hovering.
*
* @param mouseEvent The mouse move event.
*/
private void move(MouseEvent mouseEvent) {
if (game.isMining() || game.getWindow() != Window.WORLD) {
return;

View File

@ -6,8 +6,17 @@ import java.nio.charset.StandardCharsets;
import lombok.extern.slf4j.Slf4j;
/**
* Utility class responsible for loading text-based resources (e.g., ANSI art) from the classpath.
*/
@Slf4j
public class ResourceLoader {
/**
* Loads a resource file as a UTF-8 encoded string from the <code>textures/</code> directory on the classpath.
*
* @param fileName Name of the file to load (relative to <code>textures/</code>).
* @return Contents of the file as a string, or {@code null} if the file is not found or an I/O error occurs.
*/
public static String loadResource(String fileName) {
log.info("Loading resource: {}", "textures/" + fileName);
try (InputStream inputStream = ResourceLoader.class.getClassLoader()

View File

@ -1,6 +1,23 @@
package cz.jzitnik.tui;
/**
* Utility class that calculates the visible portion of the game world
* based on player position and terminal dimensions.
*/
public class ScreenMovingCalculationProvider {
/**
* Calculates the boundaries of the visible world area (in block coordinates)
* that should be rendered based on the terminal size and the player's position.
*
* @param x Player's X coordinate in the world.
* @param y Player's Y coordinate in the world.
* @param terminalHeight Height of the terminal in characters.
* @param terminalWidth Width of the terminal in characters.
* @param worldX Width of the game world in blocks.
* @param worldY Height of the game world in blocks.
* @return An array of four integers: [startX, endX, startY, endY],
* representing the visible boundaries of the world.
*/
public static int[] calculate(int x, int y, int terminalHeight, int terminalWidth, int worldX, int worldY) {
int spriteWidth = 50;
int spriteHeight = 25;

View File

@ -19,20 +19,39 @@ import java.util.ArrayList;
import java.util.List;
import java.util.Optional;
/**
* Responsible for rendering the current game state to the terminal.
* Handles multiple types of windows (e.g., world, inventory, furnace) and draws block sprites accordingly.
*/
@Slf4j
public class ScreenRenderer {
private final SpriteList spriteList;
private final Terminal terminal;
/**
* Constructs a {@code ScreenRenderer} with the given sprite list and terminal.
*
* @param spriteList List of available sprites for rendering.
* @param terminal Terminal instance used for output.
*/
public ScreenRenderer(SpriteList spriteList, Terminal terminal) {
this.spriteList = spriteList;
this.terminal = terminal;
}
/**
* Optional coordinates of the currently selected block. Used for visual highlighting.
*/
@Getter
@Setter
private Optional<List<Integer>> selectedBlock = Optional.empty();
/**
* Finds the coordinates of the player's lower or upper body in the world array.
*
* @param world The 2D world array of blocks.
* @return Integer array of coordinates [x, y], or null if player not found.
*/
private int[] getPlayerCords(List<Block>[][] world) {
for (int i = 0; i < world.length; i++) {
for (int j = 0; j < world[i].length; j++) {
@ -51,6 +70,14 @@ public class ScreenRenderer {
return null;
}
/**
* Resolves the visual texture for a given block based on its type and state.
*
* @param block The block to render.
* @param spriteList List of sprites.
* @param game Game instance (needed for state checks).
* @return String representing the block's rendered sprite.
*/
public String getTexture(Block block, SpriteList spriteList, Game game) {
if (Air.class.isAssignableFrom(spriteList.getSprite(block.getSprite()).getClass())) {
var air = (Air) spriteList.getSprite(block.getSprite());
@ -64,6 +91,12 @@ public class ScreenRenderer {
return spriteList.getSprite(block.getSprite()).getSprite(block.getSpriteState().get());
}
/**
* Renders the entire screen based on the current game window.
* Uses ANSI escape codes to clear and redraw the terminal content.
*
* @param game Current game state to render.
*/
public synchronized void render(Game game) {
log.debug("Rendering frame");
var world = game.getWorld();
@ -185,6 +218,11 @@ public class ScreenRenderer {
System.out.println(main);
}
/**
* Builds a bordered visual block (used for highlighting selected blocks).
*
* @return A StringBuilder containing a sprite-like bordered area.
*/
private static StringBuilder getStringBuilder() {
StringBuilder stringBuilder = new StringBuilder();

View File

@ -5,8 +5,35 @@ import lombok.extern.slf4j.Slf4j;
import javax.sound.sampled.*;
import java.io.IOException;
/**
* The main sound provider for the game.
* <p>
* This class is responsible for loading and playing all in-game sounds
* from the {@code resources/sounds/} directory using the Java Sound API.
* It allows different sound categories to have individual volume levels,
* while also respecting a global master volume.
* </p>
*/
@Slf4j
public class SoundPlayer {
/**
* Plays a sound file from the resources directory.
* <p>
* The method supports 16-bit PCM signed audio. It converts the input audio
* format to a standard PCM format, applies volume adjustment based on both
* the backend category volume and the user-defined master volume, and plays
* the result through the system's audio output.
* </p>
*
* @param filePath the relative file path of the sound (within the {@code sounds/} folder),
* e.g. {@code "step.ogg"}
* @param backendVolume the volume specific to the sound category (e.g. footsteps vs. mining),
* ranging from 0 (mute) to 100 (full volume)
* @param masterVolume the global volume defined in the game options, ranging from 0 to 100
* @throws LineUnavailableException if a suitable audio line cannot be opened
* @throws IOException if an I/O error occurs during reading
* @throws UnsupportedAudioFileException if the audio format is not supported
*/
public static void playSound(String filePath, int backendVolume, int masterVolume)
throws LineUnavailableException, IOException, UnsupportedAudioFileException {
if (!filePath.endsWith(".ogg") || masterVolume == 0) {
@ -54,7 +81,19 @@ public class SoundPlayer {
dataIn.close();
audioStream.close();
}
/**
*
* Applies volume adjustment to a buffer of 16-bit PCM audio samples.
* <p>
* This method modifies the input byte buffer in place by scaling each 16-bit
* signed sample by the given volume factor. Each sample is assumed to be in
* little-endian byte order (least significant byte first).
* </p>
*
* @param buffer the byte array containing 16-bit PCM audio samples
* @param bytesRead the number of bytes read into the buffer; should be a multiple of 2
* @param volume the volume multiplier (e.g., 0.5 for half volume, 2.0 for double volume)
*/
private static void applyVolume(byte[] buffer, int bytesRead, float volume) {
for (int i = 0; i < bytesRead; i += 2) { // 16-bit PCM samples are 2 bytes each
int sample = (buffer[i] & 0xFF) | (buffer[i + 1] << 8);

View File

@ -3,6 +3,12 @@ package cz.jzitnik.tui;
import java.util.HashMap;
import java.util.Optional;
/**
* Abstract class representing a sprite that can have multiple visual states.
* Each state is represented by a resource loaded from a specified location.
*
* @param <E> Enum type representing different states of the sprite.
*/
public abstract class Sprite<E extends Enum<E>> {
private HashMap<E, String> resourcesLocation;
private HashMap<E, Optional<String>> resources;
@ -10,6 +16,12 @@ public abstract class Sprite<E extends Enum<E>> {
private String defaultResourceLocation;
private Class<E> enumClass;
/**
* Loads the resource locations for each sprite state.
*
* @param values A map from enum keys to resource locations.
* @param enumClass Class object of the enum used for states.
*/
protected final void loadResources(HashMap<E, String> values, Class<E> enumClass) {
resources = new HashMap<>();
resourcesLocation = new HashMap<>();
@ -22,6 +34,12 @@ public abstract class Sprite<E extends Enum<E>> {
this.enumClass = enumClass;
}
/**
* Loads the resource for a specific sprite state from its location.
*
* @param key Enum key representing the sprite state.
* @return Loaded resource as a String.
*/
private String loadResource(E key) {
var location = resourcesLocation.get(key);
var resource = ResourceLoader.loadResource(location);
@ -31,12 +49,21 @@ public abstract class Sprite<E extends Enum<E>> {
return resource;
}
/**
* Sets the location for the default sprite resource.
*
* @param resource Path to the default resource.
*/
protected final void loadResource(String resource) {
defaultResourceLocation = resource;
}
private final String loadResource() {
/**
* Loads the default sprite resource from its location.
*
* @return Loaded default resource as a String.
*/
private String loadResource() {
var resource = ResourceLoader.loadResource(defaultResourceLocation);
defaultResource = resource;
@ -44,6 +71,13 @@ public abstract class Sprite<E extends Enum<E>> {
return resource;
}
/**
* Returns the loaded resource for the given sprite state.
* If not loaded yet, it is loaded and cached.
*
* @param key Enum key representing the sprite state.
* @return Resource as a String.
*/
protected final String getResource(E key) {
var resource = resources.get(key);
@ -54,6 +88,12 @@ public abstract class Sprite<E extends Enum<E>> {
}
}
/**
* Returns the loaded default resource.
* If not loaded yet, it is loaded and cached.
*
* @return Default resource as a String.
*/
protected final String getResource() {
if (defaultResource != null) {
return defaultResource;
@ -61,11 +101,27 @@ public abstract class Sprite<E extends Enum<E>> {
return loadResource();
}
}
/**
* Returns the sprite for the default state.
*
* @return String representing the sprite.
*/
public abstract String getSprite();
/**
* Returns the sprite for a given state.
*
* @param key Enum key representing the sprite state.
* @return String representing the sprite.
*/
public abstract String getSprite(E key);
/**
* Returns the enum class used to represent sprite states, if available. This function is used for automated tests.
*
* @return Optional containing the enum class, or empty if not set.
*/
public Optional<Class<E>> getStates() {
return Optional.ofNullable(enumClass);
}

View File

@ -3,9 +3,22 @@ package cz.jzitnik.tui;
import java.util.EnumMap;
import java.util.HashMap;
/**
* A container class that holds a mapping from enum keys to their corresponding {@link Sprite} instances.
* Ensures that a sprite is defined for every enum constant.
*
* @param <E> Enum type representing different sprite categories or states.
*/
public class SpriteList<E extends Enum<E>> {
private final EnumMap<E, Sprite> sprites;
/**
* Constructs a {@code SpriteList} and initializes it with the provided sprite mapping.
* Throws a {@link RuntimeException} if any enum constant is missing in the initial map.
*
* @param enumClass The enum class used for sprite keys.
* @param initialMap A map containing sprite instances for each enum constant.
*/
public SpriteList(Class<E> enumClass, HashMap<E, Sprite> initialMap) {
sprites = new EnumMap<>(enumClass);
@ -18,6 +31,12 @@ public class SpriteList<E extends Enum<E>> {
}
}
/**
* Returns the sprite associated with the given enum key.
*
* @param key Enum key representing the sprite category.
* @return The corresponding {@link Sprite} instance.
*/
public Sprite getSprite(E key) {
return sprites.get(key);
}

View File

@ -10,6 +10,12 @@ import cz.jzitnik.game.sprites.ui.Font.*;
import cz.jzitnik.tui.ScreenRenderer;
import lombok.extern.slf4j.Slf4j;
import java.util.Arrays;
/**
* Represents a simple interactive menu rendered in a terminal UI.
* Displays a heading and a list of buttons, handles rendering and mouse interactions.
*/
@Slf4j
public class Menu {
private Game game;
@ -23,6 +29,14 @@ public class Menu {
private int textButtonMargin;
private ButtonClickHandler buttonClickHandler;
/**
* Constructs a new Menu.
*
* @param game The game instance for accessing shared resources.
* @param headingText The main heading to display at the top of the menu.
* @param buttonLabels Array of button labels.
* @param buttonClickHandler Handler for button click actions.
*/
public Menu(Game game, String headingText, String[] buttonLabels, ButtonClickHandler buttonClickHandler) {
this.game = game;
this.headingText = headingText;
@ -31,24 +45,38 @@ public class Menu {
this.buttonClickHandler = buttonClickHandler;
}
/**
* Renders the full menu including heading and buttons into the provided buffer.
*
* @param buffer The StringBuilder buffer to append rendered output to.
* @param terminal The terminal instance, used to get terminal size.
*/
public void render(StringBuilder buffer, Terminal terminal) {
var font = game.getGameStates().dependencies.font;
var width = terminal.getWidth();
var height = terminal.getHeight();
var heading = font.line(terminal, headingText, Size.LARGE, Align.CENTER);
buffer.append(heading.getData());
mainTextHeight = heading.getHeight();
textButtonMargin = (height < 600) ? 15 : (height < 800) ? 30 : 50;
buffer.append("\n".repeat(textButtonMargin));
for (int i = 0; i < buttonLabels.length; i++) {
renderButton(buffer, buttonLabels[i], width, font, terminal, buttonsHover[i]);
buffer.append("\n".repeat(textButtonMargin / 2));
}
}
/**
* Handles mouse events related to the menu.
* Supports hover and click detection on buttons.
*
* @param mouseEvent The mouse event to handle.
* @param terminal The terminal instance.
* @param screenRenderer The renderer used to re-render the screen on hover change or click.
*/
public void handleMouse(MouseEvent mouseEvent, Terminal terminal, ScreenRenderer screenRenderer) {
int x = mouseEvent.getX();
int y = mouseEvent.getY();
@ -86,6 +114,16 @@ public class Menu {
}
}
/**
* Renders a single button to the buffer using the custom font renderer and styles.
*
* @param buffer The buffer to append rendered content to.
* @param txt The text label of the button.
* @param width The terminal width, used for calculating centering.
* @param font The font rendering utility.
* @param terminal The terminal instance.
* @param selected Whether the button is currently hovered (for styling).
*/
public void renderButton(StringBuilder buffer, String txt, int width, Font font, Terminal terminal, boolean selected) {
btnWidth = Math.min(350, (int) (width * (3.0 / 4)));
leftPad = (width - btnWidth) / 2;
@ -109,13 +147,23 @@ public class Menu {
}
}
/**
* Interface for handling button click actions.
*/
public interface ButtonClickHandler {
/**
* Called when a button is clicked.
*
* @param index The index of the clicked button.
* @param screenRenderer The renderer used to update the screen.
*/
void onClick(int index, ScreenRenderer screenRenderer);
}
/**
* Resets all hover states on the buttons (useful when switching views).
*/
public void reset() {
for (int i = 0; i < buttonsHover.length; i++) {
buttonsHover[i] = false;
}
Arrays.fill(buttonsHover, false);
}
}

View File

@ -2,7 +2,20 @@ package cz.jzitnik.tui.utils;
import cz.jzitnik.game.Game;
/**
* Utility class for rendering numeric sprites (e.g. hotbar numbers) in the terminal UI.
* Ensures fixed-size output suitable for consistent rendering in UI components like hotbars.
*/
public class Numbers {
/**
* Pads and formats a rendered number string to fit exactly into a 50x25 cell block,
* typically used in the hotbar UI.
*
* @param str The raw rendered number string (multi-line).
* @param height The actual height of the rendered string.
* @param width The actual width of the rendered string.
* @return A formatted and padded string of exactly 50x25 characters.
*/
public static String fixForHotbar(String str, int height, int width) {
var parts = str.split("\n");
// Make it 50x25
@ -24,6 +37,14 @@ public class Numbers {
return stringBuilder.toString();
}
/**
* Returns a string representation of the number formatted as a 50x25 character sprite.
* This is used to display hotbar slot numbers in a consistent visual size.
*
* @param number The number to render (19).
* @param game The game context, used to retrieve font resources.
* @return The formatted and fixed-size sprite string representing the number.
*/
public static String getNumberSprite(int number, Game game) {
if (number == 1) {
StringBuilder sprite = new StringBuilder();

View File

@ -1,6 +1,18 @@
package cz.jzitnik.tui.utils;
/**
* Utility class for combining ANSI-colored text sprites line-by-line,
* preserving non-transparent pixels (those that are not "empty").
* Useful for rendering layers (e.g. items over blocks) in a terminal UI.
*/
public class SpriteCombiner {
/**
* Combines multiple ANSI-colored text sprites into a single sprite.
* Later sprites overlay earlier ones, preserving non-transparent characters.
*
* @param sprites An array of ANSI-formatted sprite strings.
* @return A single string representing the combined sprite.
*/
public static String combineSprites(String[] sprites) {
if (sprites == null || sprites.length == 0) {
return "";
@ -15,6 +27,14 @@ public class SpriteCombiner {
return combinedSprite;
}
/**
* Combines two ANSI-formatted sprites into one, line by line.
* Characters from {@code sprite2} are used unless they are transparent (spaces with \033[0m or \033[49m).
*
* @param sprite1 The base (background) sprite.
* @param sprite2 The overlay (foreground) sprite.
* @return A string representing the resulting combined sprite.
*/
public static String combineTwoSprites(String sprite1, String sprite2) {
String[] rows1 = sprite1.split("\n");
String[] rows2 = sprite2.split("\n");
@ -69,6 +89,13 @@ public class SpriteCombiner {
return combinedSprite.toString();
}
/**
* Recursively finds the last ANSI escape code preceding the given index.
*
* @param row The sprite row string.
* @param index The index to search from.
* @return The ANSI color code at or before the index.
*/
private static String getColorCode(String row, int index) {
if (row.charAt(index) != '\033') {
return getColorCode(row, index - 1);
@ -77,6 +104,13 @@ public class SpriteCombiner {
return extractColorCode(row, index);
}
/**
* Extracts an ANSI color escape code starting at the specified index in a row.
*
* @param row The sprite row string.
* @param index The starting index (should be at '\033').
* @return The full ANSI escape code (e.g., {@code "\033[48;2;255;255;255m"}).
*/
private static String extractColorCode(String row, int index) {
StringBuilder colorCode = new StringBuilder();