Compare commits

...

4 Commits

Author SHA1 Message Date
cb488f9853 feat: Show icons in stats 2026-01-20 12:36:18 +01:00
da45765413 feat: Dialog answering 2026-01-19 20:17:13 +01:00
eef269c853 feat: Implemented text rendering 2026-01-19 17:20:49 +01:00
6335ab7e5c chore: Started implementing dialog 2026-01-19 09:30:50 +01:00
34 changed files with 875 additions and 75 deletions

View File

@@ -0,0 +1,9 @@
package cz.jzitnik.events;
import cz.jzitnik.utils.events.Event;
import lombok.Getter;
@Getter
public class QuestionAnswerEvent implements Event {
private int questionIndex;
}

View File

@@ -9,6 +9,8 @@ import cz.jzitnik.game.Constants;
import cz.jzitnik.states.RenderState; import cz.jzitnik.states.RenderState;
import cz.jzitnik.states.ScreenBuffer; import cz.jzitnik.states.ScreenBuffer;
import cz.jzitnik.states.TerminalState; import cz.jzitnik.states.TerminalState;
import cz.jzitnik.ui.pixels.AlphaPixel;
import cz.jzitnik.ui.pixels.ColoredPixel;
import cz.jzitnik.ui.pixels.Empty; import cz.jzitnik.ui.pixels.Empty;
import cz.jzitnik.ui.pixels.Pixel; import cz.jzitnik.ui.pixels.Pixel;
import cz.jzitnik.utils.DependencyManager; import cz.jzitnik.utils.DependencyManager;
@@ -41,6 +43,7 @@ public class CliHandler extends AbstractEventHandler<RerenderScreen> {
var parts = event.parts(); var parts = event.parts();
var buffer = screenBuffer.getRenderedBuffer(); var buffer = screenBuffer.getRenderedBuffer();
var globalOverrideBuffer = screenBuffer.getGlobalOverrideBuffer();
var terminalScreen = terminalState.getTerminalScreen(); var terminalScreen = terminalState.getTerminalScreen();
var tg = terminalState.getTextGraphics(); var tg = terminalState.getTextGraphics();
@@ -53,9 +56,9 @@ public class CliHandler extends AbstractEventHandler<RerenderScreen> {
for (int y = startYNormalized; y <= endYNormalized; y += 2) { for (int y = startYNormalized; y <= endYNormalized; y += 2) {
for (int x = start.getColumn(); x <= end.getColumn(); x++) { for (int x = start.getColumn(); x <= end.getColumn(); x++) {
try { try {
Pixel topPixel = buffer[y][x]; Pixel topPixel = getPixel(buffer[y][x], globalOverrideBuffer[y][x]);
Pixel bottomPixel = (y + 1 <= end.getRow()) Pixel bottomPixel = (y + 1 <= end.getRow())
? buffer[y + 1][x] ? getPixel(buffer[y + 1][x], globalOverrideBuffer[y + 1][x])
: new Empty(); : new Empty();
TextColor topColor = topPixel instanceof Empty TextColor topColor = topPixel instanceof Empty
@@ -81,6 +84,36 @@ public class CliHandler extends AbstractEventHandler<RerenderScreen> {
} }
} }
private Pixel getPixel(Pixel buffer, AlphaPixel globalOverride) {
if (globalOverride instanceof Empty) {
return buffer;
}
if (buffer instanceof Empty) {
return getPixel(new ColoredPixel(Constants.BACKGROUND_COLOR), globalOverride);
}
TextColor blended = blendColors(
buffer.getColor(),
globalOverride.getColor(),
globalOverride.getAlpha()
);
return new ColoredPixel(blended);
}
private TextColor blendColors(TextColor base, TextColor overlay, float alpha) {
int r = blend(base.getRed(), overlay.getRed(), alpha);
int g = blend(base.getGreen(), overlay.getGreen(), alpha);
int b = blend(base.getBlue(), overlay.getBlue(), alpha);
return new TextColor.RGB(r, g, b);
}
private int blend(int base, int overlay, float alpha) {
return Math.round(base * (1 - alpha) + overlay * alpha);
}
private void drawHalfPixel(TextGraphics tg, int x, int y, private void drawHalfPixel(TextGraphics tg, int x, int y,
TextColor topColor, TextColor topColor,
TextColor bottomColor) { TextColor bottomColor) {

View File

@@ -0,0 +1,191 @@
package cz.jzitnik.events.handlers;
import com.googlecode.lanterna.TerminalPosition;
import com.googlecode.lanterna.TerminalSize;
import com.googlecode.lanterna.TextColor;
import cz.jzitnik.annotations.EventHandler;
import cz.jzitnik.annotations.injectors.InjectDependency;
import cz.jzitnik.annotations.injectors.InjectState;
import cz.jzitnik.events.RerenderScreen;
import cz.jzitnik.game.dialog.Dialog;
import cz.jzitnik.game.dialog.OnEnd;
import cz.jzitnik.states.DialogState;
import cz.jzitnik.states.ScreenBuffer;
import cz.jzitnik.states.TerminalState;
import cz.jzitnik.ui.pixels.AlphaPixel;
import cz.jzitnik.ui.pixels.ColoredPixel;
import cz.jzitnik.ui.pixels.Empty;
import cz.jzitnik.utils.DependencyManager;
import cz.jzitnik.utils.TextRenderer;
import cz.jzitnik.utils.events.AbstractEventHandler;
import cz.jzitnik.utils.events.EventManager;
import lombok.extern.slf4j.Slf4j;
import java.awt.*;
import java.util.List;
import java.util.ArrayList;
@Slf4j
@EventHandler(Dialog.class)
public class DialogEventHandler extends AbstractEventHandler<Dialog> {
public DialogEventHandler(DependencyManager dm) {
super(dm);
}
@InjectState
private DialogState dialogState;
@InjectState
private TerminalState terminalState;
@InjectState
private ScreenBuffer screenBuffer;
@InjectDependency
private EventManager eventManager;
@InjectDependency
private TextRenderer textRenderer;
private static final int WIDTH = 350;
private static final int MARGIN_BOTTOM = 15;
public static final int PADDING = 7;
private static final int BUTTON_TEXT_PADDING = 4;
private static final int QUESTION_ACTIONS_GAP = 10;
public static final int BUTTON_HEIGHT = 15;
public static final int BUTTON_PADDING = 5;
private static final float FONT_SIZE = 15f;
public static int calculateButtonHeight(Dialog dialog) {
if (dialog.getOnEnd() instanceof OnEnd.AskQuestion(OnEnd.AskQuestion.Answer[] answers)) {
return answers.length * BUTTON_HEIGHT + (answers.length - 1) * BUTTON_PADDING;
} else {
return 0;
}
}
public static int getYStartButtons(TextRenderer textRenderer, Dialog dialog) {
var textSize = textRenderer.measureText(dialog.getText(), WIDTH - PADDING * 2, FONT_SIZE);
return PADDING + textSize.height + BUTTON_PADDING;
}
public static TerminalSize getSize(TextRenderer textRenderer, Dialog dialog) {
var textSize = textRenderer.measureText(dialog.getText(), WIDTH - PADDING * 2, FONT_SIZE);
return new TerminalSize(300, PADDING + textSize.height + (
dialog.getOnEnd() instanceof OnEnd.AskQuestion ? BUTTON_PADDING + calculateButtonHeight(dialog) : 0
) + PADDING);
}
public static TerminalPosition getStart(TerminalSize terminalSize, TerminalSize size) {
int startY = terminalSize.getRows() * 2 - MARGIN_BOTTOM - size.getRows();
int startX = (terminalSize.getColumns() / 2) - (size.getColumns() / 2);
return new TerminalPosition(startX, startY);
}
@Override
public void handle(Dialog event) {
boolean onlyLast = dialogState.getCurrentDialog() == event;
dialogState.setCurrentDialog(event);
TerminalSize terminalSize = terminalState.getTerminalScreen().getTerminalSize();
var overrideBuffer = screenBuffer.getGlobalOverrideBuffer();
var size = getSize(textRenderer, event);
var start = getStart(terminalSize, size);
var animation = textRenderer.renderTypingAnimation(event.getText(), size.getColumns() - PADDING * 2, size.getRows() - PADDING * 2, Color.WHITE, FONT_SIZE);
var textSize = textRenderer.measureText(event.getText(), size.getColumns() - PADDING * 2, FONT_SIZE);
OnEnd onEnd = event.getOnEnd();
List<AlphaPixel[][]> answersBuf = new ArrayList<>();
if (onEnd instanceof OnEnd.AskQuestion(
OnEnd.AskQuestion.Answer[] answers
)) {
for (OnEnd.AskQuestion.Answer answer : answers) {
answersBuf.add(textRenderer.renderText(answer.answer(), size.getColumns() - PADDING * 2, BUTTON_HEIGHT, Color.BLACK, FONT_SIZE));
}
}
dialogState.setRenderInProgress(true);
try {
for (int i = onlyLast ? animation.length : 0; i <= animation.length; i++) {
var buf = animation[Math.min(i, animation.length - 1)];
for (int y = 0; y < size.getRows(); y++) {
for (int x = 0; x < size.getColumns(); x++) {
var textPixel = buf[Math.min(Math.max(0, y - PADDING), buf.length - 1)][Math.min(Math.max(0, x - PADDING), buf[0].length - 1)];
if (textPixel instanceof Empty || y < PADDING || x < PADDING || x >= size.getColumns() - PADDING || y >= size.getRows() - PADDING) {
if (i == animation.length && y - 2 > textSize.height + QUESTION_ACTIONS_GAP && onEnd instanceof OnEnd.AskQuestion(
OnEnd.AskQuestion.Answer[] answers
)) {
int buttonsY = y - textSize.height - QUESTION_ACTIONS_GAP - 2;
int buttonIndex = buttonsY / (BUTTON_HEIGHT + BUTTON_PADDING);
int rest = buttonsY % (BUTTON_HEIGHT + BUTTON_PADDING);
if (buttonIndex < answers.length && rest < BUTTON_HEIGHT && x >= PADDING && x < size.getColumns() - PADDING) {
int localY = rest - BUTTON_TEXT_PADDING;
int localX = x - PADDING - BUTTON_TEXT_PADDING;
var buttonBuf = answersBuf.get(buttonIndex);
var buttonTextPixel = buttonBuf[Math.min(Math.max(0, localY), buttonBuf.length - 1)][Math.min(Math.max(0, localX), buttonBuf[0].length - 1)];
if (buttonTextPixel instanceof Empty || localY < 0 || localX < 0 || localY >= buttonBuf.length || localX >= buttonBuf[0].length) {
overrideBuffer[start.getRow() + y][start.getColumn() + x] = new ColoredPixel(new TextColor.RGB(255, 255, 255), dialogState.getHoveredButtonIndex() == buttonIndex ? 0.8f : 0.6f);
} else {
overrideBuffer[start.getRow() + y][start.getColumn() + x] = buttonTextPixel;
}
continue;
}
}
overrideBuffer[start.getRow() + y][start.getColumn() + x] = new ColoredPixel(new TextColor.RGB(0, 0, 0), 0.6f);
continue;
}
overrideBuffer[start.getRow() + y][start.getColumn() + x] = textPixel;
}
}
eventManager.emitEvent(
new RerenderScreen(
new RerenderScreen.ScreenPart(
start,
new TerminalPosition(start.getColumn() + size.getColumns(), start.getRow() + size.getRows())
)
)
);
Thread.sleep(1000 / event.getTypingSpeed());
}
dialogState.setRenderInProgress(false);
if (onEnd instanceof OnEnd.Continue(Dialog nextDialog)) {
Thread.sleep(1000);
for (int y = start.getRow(); y < start.getRow() + size.getRows(); y++) {
for (int x = start.getColumn(); x < start.getColumn() + size.getColumns(); x++) {
screenBuffer.getGlobalOverrideBuffer()[y][x] = new Empty();
}
}
if (nextDialog == null) {
dialogState.setCurrentDialog(null);
eventManager.emitEvent(
new RerenderScreen(
new RerenderScreen.ScreenPart(
start,
new TerminalPosition(start.getColumn() + size.getColumns(), start.getRow() + size.getRows())
)
)
);
} else {
eventManager.emitEvent(nextDialog);
}
}
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
}

View File

@@ -76,7 +76,10 @@ public class MouseActionEventHandler extends AbstractEventHandler<MouseAction> {
gameState.getPlayer().swing(playerConfig.getSwingTimeMs()); gameState.getPlayer().swing(playerConfig.getSwingTimeMs());
object.ifPresent(selectable -> selectable.interact(dm)); object.ifPresent(selectable -> {
dm.inject(selectable);
selectable.interact();
});
} }
default -> uiRoomClickHandlerRepository.handleElse(event); default -> uiRoomClickHandlerRepository.handleElse(event);
} }

View File

@@ -0,0 +1,35 @@
package cz.jzitnik.events.handlers;
import cz.jzitnik.annotations.EventHandler;
import cz.jzitnik.annotations.injectors.InjectDependency;
import cz.jzitnik.annotations.injectors.InjectState;
import cz.jzitnik.events.QuestionAnswerEvent;
import cz.jzitnik.game.dialog.Dialog;
import cz.jzitnik.game.dialog.OnEnd;
import cz.jzitnik.states.DialogState;
import cz.jzitnik.utils.DependencyManager;
import cz.jzitnik.utils.events.AbstractEventHandler;
import cz.jzitnik.utils.events.EventManager;
@EventHandler(QuestionAnswerEvent.class)
public class QuestionAnswerEventHandler extends AbstractEventHandler<QuestionAnswerEvent> {
public QuestionAnswerEventHandler(DependencyManager dm) {
super(dm);
}
@InjectState
private DialogState dialogState;
@InjectDependency
private EventManager eventManager;
@Override
public void handle(QuestionAnswerEvent event) {
OnEnd dialogOnEnd = dialogState.getCurrentDialog().getOnEnd();
if (dialogOnEnd instanceof OnEnd.AskQuestion dialog) {
OnEnd.AskQuestion.Answer answer = dialog.answers()[event.getQuestionIndex()];
Dialog switchTo = answer.dialog();
}
}
}

View File

@@ -8,6 +8,7 @@ import cz.jzitnik.events.FullRoomDraw;
import cz.jzitnik.events.TerminalResizeEvent; import cz.jzitnik.events.TerminalResizeEvent;
import cz.jzitnik.game.GameState; import cz.jzitnik.game.GameState;
import cz.jzitnik.states.ScreenBuffer; import cz.jzitnik.states.ScreenBuffer;
import cz.jzitnik.ui.pixels.AlphaPixel;
import cz.jzitnik.ui.pixels.Empty; import cz.jzitnik.ui.pixels.Empty;
import cz.jzitnik.ui.pixels.Pixel; import cz.jzitnik.ui.pixels.Pixel;
import cz.jzitnik.utils.DependencyManager; import cz.jzitnik.utils.DependencyManager;
@@ -40,12 +41,15 @@ public class TerminalResizeEventHandler extends AbstractEventHandler<TerminalRes
int height = size.getRows() * 2; int height = size.getRows() * 2;
Pixel[][] buffer = new Pixel[height][width]; Pixel[][] buffer = new Pixel[height][width];
AlphaPixel[][] globalOverride = new AlphaPixel[height][width];
for (int x = 0; x < width; x++) { for (int x = 0; x < width; x++) {
for (int y = 0; y < height; y++) { for (int y = 0; y < height; y++) {
buffer[y][x] = new Empty(); buffer[y][x] = new Empty();
globalOverride[y][x] = new Empty();
} }
} }
screenBuffer.setRenderedBuffer(buffer); screenBuffer.setRenderedBuffer(buffer);
screenBuffer.setGlobalOverrideBuffer(globalOverride);
if (gameState.getScreen() != null) { if (gameState.getScreen() != null) {
if (screenRerendering) { if (screenRerendering) {

View File

@@ -1,6 +1,7 @@
package cz.jzitnik.game; package cz.jzitnik.game;
import cz.jzitnik.annotations.State; import cz.jzitnik.annotations.State;
import cz.jzitnik.game.dialog.Dialog;
import cz.jzitnik.game.objects.Interactable; import cz.jzitnik.game.objects.Interactable;
import cz.jzitnik.screens.Screen; import cz.jzitnik.screens.Screen;
import cz.jzitnik.utils.DependencyManager; import cz.jzitnik.utils.DependencyManager;

View File

@@ -34,7 +34,6 @@ public class Player {
private PlayerRotation playerRotation = PlayerRotation.FRONT; private PlayerRotation playerRotation = PlayerRotation.FRONT;
@Setter @Setter
private GameItem selectedItem; private GameItem selectedItem;
@Setter
private int health = MAX_HEALTH; private int health = MAX_HEALTH;
private int stamina = MAX_STAMINA; private int stamina = MAX_STAMINA;
private boolean swinging = false; private boolean swinging = false;

View File

@@ -36,7 +36,10 @@ public class ResourceManager {
APPLE("food/apple.png"), APPLE("food/apple.png"),
DOORS("rooms/doors.png"); DOORS("rooms/doors.png"),
STAMINA("ui/stamina.png"),
HEART("ui/heart.png");
private final String path; private final String path;
} }

View File

@@ -0,0 +1,18 @@
package cz.jzitnik.game.dialog;
import cz.jzitnik.utils.events.Event;
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.RequiredArgsConstructor;
@AllArgsConstructor
@RequiredArgsConstructor
@Getter
public class Dialog implements Event {
/**
* Characters per second
*/
private int typingSpeed = 10;
private final String text;
private final OnEnd onEnd;
}

View File

@@ -0,0 +1,11 @@
package cz.jzitnik.game.dialog;
public interface OnEnd {
record Continue(Dialog nextDialog) implements OnEnd {
}
record AskQuestion(Answer[] answers) implements OnEnd {
public record Answer(String answer, Dialog dialog) {
}
}
}

View File

@@ -0,0 +1,41 @@
package cz.jzitnik.game.mobs;
import cz.jzitnik.annotations.injectors.InjectDependency;
import cz.jzitnik.annotations.injectors.InjectState;
import cz.jzitnik.game.dialog.Dialog;
import cz.jzitnik.game.utils.RoomCords;
import cz.jzitnik.states.DialogState;
import cz.jzitnik.utils.events.EventManager;
import cz.jzitnik.utils.roomtasks.RoomTask;
import lombok.extern.slf4j.Slf4j;
import java.awt.image.BufferedImage;
@Slf4j
public abstract class DialogMob extends Mob {
private final Dialog dialog;
public DialogMob(BufferedImage texture, RoomTask[] tasks, RoomCords cords, Dialog dialog) {
super(texture, tasks, cords);
this.dialog = dialog;
}
public DialogMob(BufferedImage texture, RoomTask task, RoomCords cords, Dialog dialog) {
super(texture, task, cords);
this.dialog = dialog;
}
@InjectDependency
private EventManager eventManager;
@InjectState
private DialogState dialogState;
@Override
public void interact() {
log.debug("Interacting with dialog mob!");
if (dialogState.getCurrentDialog() == null) {
eventManager.emitEvent(dialog);
}
}
}

View File

@@ -73,9 +73,7 @@ public abstract class HittableMob extends Mob {
} }
@Override @Override
public final void interact(DependencyManager dm) { public final void interact() {
dm.inject(this);
health -= gameState.getPlayer().getDamageDeal(); health -= gameState.getPlayer().getDamageDeal();
log.debug("Health: {}", health); log.debug("Health: {}", health);

View File

@@ -72,8 +72,7 @@ public final class Chest extends GameObject implements UIClickHandler {
} }
@Override @Override
public void interact(DependencyManager dm) { public void interact() {
dm.inject(this);
log.debug("Interacted with chest"); log.debug("Interacted with chest");
render(false); render(false);
} }

View File

@@ -1,5 +1,7 @@
package cz.jzitnik.game.objects; package cz.jzitnik.game.objects;
import cz.jzitnik.annotations.injectors.InjectDependency;
import cz.jzitnik.annotations.injectors.InjectState;
import cz.jzitnik.events.DroppedItemRerender; import cz.jzitnik.events.DroppedItemRerender;
import cz.jzitnik.events.InventoryRerender; import cz.jzitnik.events.InventoryRerender;
import cz.jzitnik.game.GameRoom; import cz.jzitnik.game.GameRoom;
@@ -31,18 +33,19 @@ public final class DroppedItem implements Selectable, Serializable {
return item.getTexture(); return item.getTexture();
} }
@Override @InjectState
public void interact(DependencyManager dm) { private GameState gameState;
StateManager stateManager = dm.getDependencyOrThrow(StateManager.class);
GameState gameState = stateManager.getOrThrow(GameState.class);
@InjectDependency
private EventManager eventManager;
@Override
public void interact() {
if (!gameState.getPlayer().addItem(item)) { if (!gameState.getPlayer().addItem(item)) {
return; return;
} }
EventManager eventManager = dm.getDependencyOrThrow(EventManager.class); gameState.getCurrentRoom().getDroppedItems().remove(this);
var currentRoom = gameState.getCurrentRoom();
currentRoom.getDroppedItems().remove(this);
eventManager.emitEvent(new InventoryRerender()); eventManager.emitEvent(new InventoryRerender());
eventManager.emitEvent(new DroppedItemRerender(this)); eventManager.emitEvent(new DroppedItemRerender(this));
} }

View File

@@ -1,7 +1,5 @@
package cz.jzitnik.game.objects; package cz.jzitnik.game.objects;
import cz.jzitnik.utils.DependencyManager;
public interface Interactable { public interface Interactable {
void interact(DependencyManager dm); void interact();
} }

View File

@@ -1,14 +0,0 @@
package cz.jzitnik.game.setup.config;
import cz.jzitnik.game.ResourceManager;
public class EnemiesConfigSetup {
private class Enemy {
private ResourceManager.Resource texture;
private int initialHealth;
}
public void setup() {
}
}

View File

@@ -0,0 +1,34 @@
package cz.jzitnik.game.setup.enemies;
import cz.jzitnik.game.ResourceManager;
import cz.jzitnik.game.dialog.Dialog;
import cz.jzitnik.game.dialog.OnEnd;
import cz.jzitnik.game.mobs.DialogMob;
import cz.jzitnik.game.utils.RoomCords;
import cz.jzitnik.utils.roomtasks.RoomTask;
public class Pepa extends DialogMob {
public Pepa(ResourceManager resourceManager, RoomCords cords) {
super(resourceManager.getResource(ResourceManager.Resource.PLAYER_FRONT), new RoomTask[]{}, cords,
new Dialog(
"Pepa: Never gonna give you up",
new OnEnd.Continue(new Dialog(
"Pepa: Never gonna let you down",
new OnEnd.Continue(new Dialog(
"Pepa: Never gonna run around",
new OnEnd.Continue(new Dialog(
"Pepa: How it continues?", new OnEnd.AskQuestion(
new OnEnd.AskQuestion.Answer[]{
new OnEnd.AskQuestion.Answer("And desert you", new Dialog("Pepa: You are god damn right!", new OnEnd.Continue(null))),
new OnEnd.AskQuestion.Answer("You are a dessert", new Dialog("Pepa: WRONG!", new OnEnd.Continue(null)))
}
)))
))
))
)
);
setTasks(new RoomTask[]{
});
}
}

View File

@@ -4,6 +4,7 @@ import cz.jzitnik.game.GameRoom;
import cz.jzitnik.game.GameRoomPart; import cz.jzitnik.game.GameRoomPart;
import cz.jzitnik.game.ResourceManager; import cz.jzitnik.game.ResourceManager;
import cz.jzitnik.game.items.GameItem; import cz.jzitnik.game.items.GameItem;
import cz.jzitnik.game.setup.enemies.Pepa;
import cz.jzitnik.game.setup.items.Apple; import cz.jzitnik.game.setup.items.Apple;
import cz.jzitnik.game.setup.items.WoodenSword; import cz.jzitnik.game.setup.items.WoodenSword;
import cz.jzitnik.game.objects.Chest; import cz.jzitnik.game.objects.Chest;
@@ -26,7 +27,9 @@ public class MainRoom extends GameRoom {
)); ));
addObject(chest); addObject(chest);
Zombie zombie = new Zombie(resourceManager, new RoomCords(100, 100)); //Zombie zombie = new Zombie(resourceManager, new RoomCords(100, 100));
addMob(zombie); //addMob(zombie);
Pepa pepa = new Pepa(resourceManager, new RoomCords(100, 100));
addMob(pepa);
} }
} }

View File

@@ -1,4 +1,4 @@
package cz.jzitnik.game.setup; package cz.jzitnik.game.setup.scenes;
import com.googlecode.lanterna.input.KeyType; import com.googlecode.lanterna.input.KeyType;
import cz.jzitnik.annotations.injectors.InjectDependency; import cz.jzitnik.annotations.injectors.InjectDependency;

View File

@@ -1,4 +1,4 @@
package cz.jzitnik.game.setup; package cz.jzitnik.game.setup.scenes;
import cz.jzitnik.screens.Screen; import cz.jzitnik.screens.Screen;
import cz.jzitnik.screens.scenes.Scene; import cz.jzitnik.screens.scenes.Scene;

View File

@@ -0,0 +1,13 @@
package cz.jzitnik.states;
import cz.jzitnik.annotations.State;
import cz.jzitnik.game.dialog.Dialog;
import lombok.Data;
@State
@Data
public class DialogState {
private Dialog currentDialog;
private boolean renderInProgress = false;
private int hoveredButtonIndex = -1;
}

View File

@@ -1,6 +1,7 @@
package cz.jzitnik.states; package cz.jzitnik.states;
import cz.jzitnik.annotations.State; import cz.jzitnik.annotations.State;
import cz.jzitnik.ui.pixels.AlphaPixel;
import cz.jzitnik.ui.pixels.Pixel; import cz.jzitnik.ui.pixels.Pixel;
import lombok.Data; import lombok.Data;
@@ -8,4 +9,5 @@ import lombok.Data;
@State @State
public class ScreenBuffer { public class ScreenBuffer {
private Pixel[][] renderedBuffer = new Pixel[][] {}; private Pixel[][] renderedBuffer = new Pixel[][] {};
private AlphaPixel[][] globalOverrideBuffer = new AlphaPixel[][] {};
} }

View File

@@ -0,0 +1,164 @@
package cz.jzitnik.ui;
import com.googlecode.lanterna.TerminalPosition;
import com.googlecode.lanterna.TerminalSize;
import cz.jzitnik.annotations.Dependency;
import cz.jzitnik.annotations.injectors.InjectDependency;
import cz.jzitnik.annotations.injectors.InjectState;
import cz.jzitnik.annotations.ui.MouseHandler;
import cz.jzitnik.annotations.ui.MouseHandlerType;
import cz.jzitnik.annotations.ui.UI;
import cz.jzitnik.events.MouseAction;
import cz.jzitnik.events.RerenderScreen;
import cz.jzitnik.events.handlers.DialogEventHandler;
import cz.jzitnik.game.dialog.OnEnd;
import cz.jzitnik.states.DialogState;
import cz.jzitnik.states.ScreenBuffer;
import cz.jzitnik.states.TerminalState;
import cz.jzitnik.ui.pixels.Empty;
import cz.jzitnik.utils.TextRenderer;
import cz.jzitnik.utils.events.Event;
import cz.jzitnik.utils.events.EventManager;
import lombok.extern.slf4j.Slf4j;
import static cz.jzitnik.events.handlers.DialogEventHandler.*;
@Slf4j
@UI
@Dependency
public class DialogUI {
@InjectState
private DialogState dialogState;
@InjectState
private TerminalState terminalState;
@InjectDependency
private TextRenderer textRenderer;
@InjectDependency
private EventManager eventManager;
@InjectState
private ScreenBuffer screenBuffer;
@MouseHandler(MouseHandlerType.CLICK)
public boolean handleClick(MouseAction mouseAction) {
if (dialogState.getCurrentDialog() == null || dialogState.isRenderInProgress()) {
return false;
}
TerminalSize size = DialogEventHandler.getSize(textRenderer, dialogState.getCurrentDialog());
TerminalPosition start = DialogEventHandler.getStart(terminalState.getTerminalScreen().getTerminalSize(), size);
if (!(dialogState.getCurrentDialog().getOnEnd() instanceof OnEnd.AskQuestion(
OnEnd.AskQuestion.Answer[] answers
))) {
setHoveredButtonIndex(-1);
return false;
}
TerminalPosition mouse = mouseAction.getPosition();
TerminalPosition mouseNormalized = new TerminalPosition(mouse.getColumn(), mouse.getRow() * 2);
RerenderScreen.ScreenPart part = new RerenderScreen.ScreenPart(
start,
new TerminalPosition(start.getColumn() + size.getColumns(), start.getRow() + size.getRows())
);
if (!part.isWithin(mouseNormalized)) {
setHoveredButtonIndex(-1);
return false;
}
int buttonsStartY = DialogEventHandler.getYStartButtons(textRenderer, dialogState.getCurrentDialog());
TerminalPosition localPosition = new TerminalPosition(mouseNormalized.getColumn() - start.getColumn(), mouseNormalized.getRow() - start.getRow() - buttonsStartY);
int buttonsHeight = DialogEventHandler.calculateButtonHeight(dialogState.getCurrentDialog());
if (localPosition.getRow() < 0 || localPosition.getRow() >= buttonsHeight) {
setHoveredButtonIndex(-1);
return true;
}
int buttonIndex = localPosition.getRow() / (BUTTON_HEIGHT + BUTTON_PADDING);
int rest = localPosition.getRow() % (BUTTON_HEIGHT + BUTTON_PADDING);
if (buttonIndex < answers.length && rest < BUTTON_HEIGHT && localPosition.getColumn() >= PADDING && localPosition.getColumn() < size.getColumns() - PADDING) {
for (int y = start.getRow(); y < start.getRow() + size.getRows(); y++) {
for (int x = start.getColumn(); x < start.getColumn() + size.getColumns(); x++) {
screenBuffer.getGlobalOverrideBuffer()[y][x] = new Empty();
}
}
eventManager.emitEvent(
new Event[]{
new RerenderScreen(
new RerenderScreen.ScreenPart(
start,
new TerminalPosition(start.getColumn() + size.getColumns(), start.getRow() + size.getRows())
)
),
answers[buttonIndex].dialog(),
});
return true;
}
return true;
}
@MouseHandler(MouseHandlerType.MOVE)
public boolean handleMove(MouseAction mouseAction) {
if (dialogState.getCurrentDialog() == null || dialogState.isRenderInProgress()) {
return false;
}
TerminalSize size = DialogEventHandler.getSize(textRenderer, dialogState.getCurrentDialog());
TerminalPosition start = DialogEventHandler.getStart(terminalState.getTerminalScreen().getTerminalSize(), size);
if (!(dialogState.getCurrentDialog().getOnEnd() instanceof OnEnd.AskQuestion(
OnEnd.AskQuestion.Answer[] answers
))) {
setHoveredButtonIndex(-1);
return false;
}
TerminalPosition mouse = mouseAction.getPosition();
TerminalPosition mouseNormalized = new TerminalPosition(mouse.getColumn(), mouse.getRow() * 2);
RerenderScreen.ScreenPart part = new RerenderScreen.ScreenPart(
start,
new TerminalPosition(start.getColumn() + size.getColumns(), start.getRow() + size.getRows())
);
if (!part.isWithin(mouseNormalized)) {
setHoveredButtonIndex(-1);
return false;
}
int buttonsStartY = DialogEventHandler.getYStartButtons(textRenderer, dialogState.getCurrentDialog());
TerminalPosition localPosition = new TerminalPosition(mouseNormalized.getColumn() - start.getColumn(), mouseNormalized.getRow() - start.getRow() - buttonsStartY);
int buttonsHeight = DialogEventHandler.calculateButtonHeight(dialogState.getCurrentDialog());
if (localPosition.getRow() < 0 || localPosition.getRow() >= buttonsHeight) {
setHoveredButtonIndex(-1);
return true;
}
int buttonIndex = localPosition.getRow() / (BUTTON_HEIGHT + BUTTON_PADDING);
int rest = localPosition.getRow() % (BUTTON_HEIGHT + BUTTON_PADDING);
if (buttonIndex < answers.length && rest < BUTTON_HEIGHT && localPosition.getColumn() >= PADDING && localPosition.getColumn() < size.getColumns() - PADDING) {
setHoveredButtonIndex(buttonIndex);
return true;
}
setHoveredButtonIndex(-1);
return true;
}
private void setHoveredButtonIndex(int index) {
if (dialogState.getHoveredButtonIndex() != index) {
dialogState.setHoveredButtonIndex(index);
eventManager.emitEvent(dialogState.getCurrentDialog());
}
}
}

View File

@@ -2,27 +2,34 @@ package cz.jzitnik.ui;
import com.googlecode.lanterna.TextColor; import com.googlecode.lanterna.TextColor;
import cz.jzitnik.annotations.Dependency; import cz.jzitnik.annotations.Dependency;
import cz.jzitnik.annotations.injectors.InjectDependency;
import cz.jzitnik.annotations.injectors.InjectState; import cz.jzitnik.annotations.injectors.InjectState;
import cz.jzitnik.annotations.ui.Render; import cz.jzitnik.annotations.ui.Render;
import cz.jzitnik.annotations.ui.UI; import cz.jzitnik.annotations.ui.UI;
import cz.jzitnik.game.GameState; import cz.jzitnik.game.GameState;
import cz.jzitnik.game.Player; import cz.jzitnik.game.Player;
import cz.jzitnik.game.ResourceManager;
import cz.jzitnik.states.ScreenBuffer; import cz.jzitnik.states.ScreenBuffer;
import cz.jzitnik.ui.pixels.ColoredPixel; import cz.jzitnik.ui.pixels.ColoredPixel;
import cz.jzitnik.ui.pixels.Empty; import cz.jzitnik.ui.pixels.Empty;
import cz.jzitnik.ui.pixels.Pixel; import cz.jzitnik.ui.pixels.Pixel;
import cz.jzitnik.utils.RerenderUtils;
import lombok.Getter; import lombok.Getter;
import java.awt.image.BufferedImage;
@Getter @Getter
@UI @UI
@Dependency @Dependency
public class Stats { public class Stats {
public static final int BARS_COUNT = 2; public static final int BARS_COUNT = 2;
public static final int BAR_WIDTH = 72; public static final int BAR_WIDTH = 72;
public static final int ICON_SIZE = 9;
public static final int ICON_BAR_MARGIN = 2;
public static final int BAR_HEIGHT = 8; public static final int BAR_HEIGHT = 8;
public static final int BAR_PADDING = 2; public static final int BAR_PADDING = 2;
public static final int HEIGHT = BAR_HEIGHT * BARS_COUNT + (BAR_PADDING * (BARS_COUNT - 1)); public static final int HEIGHT = BAR_HEIGHT * BARS_COUNT + (BAR_PADDING * (BARS_COUNT - 1));
public static final int WIDTH = BAR_WIDTH; public static final int WIDTH = BAR_WIDTH + ICON_SIZE + ICON_BAR_MARGIN;
public static final int OFFSET_X = 5; public static final int OFFSET_X = 5;
public static final int OFFSET_Y = 5; public static final int OFFSET_Y = 5;
@@ -36,6 +43,9 @@ public class Stats {
@InjectState @InjectState
private ScreenBuffer screenBuffer; private ScreenBuffer screenBuffer;
@InjectDependency
private ResourceManager resourceManager;
@Render @Render
public void rerender() { public void rerender() {
var buffer = screenBuffer.getRenderedBuffer(); var buffer = screenBuffer.getRenderedBuffer();
@@ -49,20 +59,38 @@ public class Stats {
for (int x = 0; x < BAR_WIDTH; x++) { for (int x = 0; x < BAR_WIDTH; x++) {
for (int y = 0; y < BAR_HEIGHT; y++) { for (int y = 0; y < BAR_HEIGHT; y++) {
if (x == 0 || y == 0 || x == BAR_WIDTH - 1 || y == BAR_HEIGHT - 1 || x - 1 < healthAmount) { if (x == 0 || y == 0 || x == BAR_WIDTH - 1 || y == BAR_HEIGHT - 1 || x - 1 < healthAmount) {
buffer[y + OFFSET_Y][x + OFFSET_X] = HEALTH_COLOR; buffer[y + OFFSET_Y][x + OFFSET_X + ICON_SIZE + ICON_BAR_MARGIN] = HEALTH_COLOR;
} else { } else {
buffer[y + OFFSET_Y][x + OFFSET_X] = new Empty(); buffer[y + OFFSET_Y][x + OFFSET_X + ICON_SIZE + ICON_BAR_MARGIN] = new Empty();
} }
} }
} }
BufferedImage heartImage = resourceManager.getResource(ResourceManager.Resource.HEART);
for (int x = 0; x < heartImage.getWidth(); x++) {
for (int y = 0; y < heartImage.getHeight(); y++) {
var pixelData = RerenderUtils.getPixelData(heartImage.getRGB(x, y));
buffer[y + OFFSET_Y][x + OFFSET_X] = new ColoredPixel(new TextColor.RGB(pixelData.r(), pixelData.g(), pixelData.b()));
}
}
for (int x = 0; x < BAR_WIDTH; x++) { for (int x = 0; x < BAR_WIDTH; x++) {
for (int y = BAR_HEIGHT + BAR_PADDING; y < BAR_PADDING + BAR_HEIGHT * 2; y++) { for (int y = BAR_HEIGHT + BAR_PADDING; y < BAR_PADDING + BAR_HEIGHT * 2; y++) {
if (x == 0 || y == BAR_HEIGHT + BAR_PADDING || x == BAR_WIDTH - 1 || y == BAR_PADDING + BAR_HEIGHT * 2 - 1 || x - 1 < staminaAmount) { if (x == 0 || y == BAR_HEIGHT + BAR_PADDING || x == BAR_WIDTH - 1 || y == BAR_PADDING + BAR_HEIGHT * 2 - 1 || x - 1 < staminaAmount) {
buffer[y + OFFSET_Y][x + OFFSET_X] = STAMINA_COLOR; buffer[y + OFFSET_Y][x + OFFSET_X + ICON_SIZE + ICON_BAR_MARGIN] = STAMINA_COLOR;
} else { } else {
buffer[y + OFFSET_Y][x + OFFSET_X] = new Empty(); buffer[y + OFFSET_Y][x + OFFSET_X + ICON_SIZE + ICON_BAR_MARGIN] = new Empty();
} }
}
}
BufferedImage staminaImage = resourceManager.getResource(ResourceManager.Resource.STAMINA);
for (int x = 0; x < staminaImage.getWidth(); x++) {
for (int y = 0; y < staminaImage.getHeight(); y++) {
var pixelData = RerenderUtils.getPixelData(staminaImage.getRGB(x, y));
buffer[y + OFFSET_Y + BAR_HEIGHT + BAR_PADDING][x + OFFSET_X] = new ColoredPixel(new TextColor.RGB(pixelData.r(), pixelData.g(), pixelData.b()));
} }
} }
} }

View File

@@ -0,0 +1,13 @@
package cz.jzitnik.ui.pixels;
import com.googlecode.lanterna.TextColor;
import lombok.Getter;
@Getter
public sealed abstract class AlphaPixel extends Pixel permits Empty, ColoredPixel {
private final float alpha;
public AlphaPixel(TextColor color, float alpha) {
super(color);
this.alpha = alpha;
}
}

View File

@@ -2,8 +2,12 @@ package cz.jzitnik.ui.pixels;
import com.googlecode.lanterna.TextColor; import com.googlecode.lanterna.TextColor;
public final class ColoredPixel extends Pixel { public final class ColoredPixel extends AlphaPixel {
public ColoredPixel(TextColor color) { public ColoredPixel(TextColor color) {
super(color); super(color, 1f);
}
public ColoredPixel(TextColor color, float alpha) {
super(color, alpha);
} }
} }

View File

@@ -1,7 +1,7 @@
package cz.jzitnik.ui.pixels; package cz.jzitnik.ui.pixels;
public final class Empty extends Pixel { public final class Empty extends AlphaPixel {
public Empty() { public Empty() {
super(null); super(null, 0f);
} }
} }

View File

@@ -6,6 +6,6 @@ import lombok.Getter;
@Getter @Getter
@AllArgsConstructor @AllArgsConstructor
public sealed abstract class Pixel permits Empty, ColoredPixel { public sealed abstract class Pixel permits AlphaPixel {
protected TextColor color; protected TextColor color;
} }

View File

@@ -95,17 +95,15 @@ public class RerenderUtils {
RoomCords endObjectCords = new RoomCords(startObjectCords.getX() + texture.getWidth() - 1, startObjectCords.getY() + texture.getHeight() - 1); RoomCords endObjectCords = new RoomCords(startObjectCords.getX() + texture.getWidth() - 1, startObjectCords.getY() + texture.getHeight() - 1);
if (x >= startObjectCords.getX() && x <= endObjectCords.getX() && y >= startObjectCords.getY() && y <= endObjectCords.getY()) { if (x >= startObjectCords.getX() && x <= endObjectCords.getX() && y >= startObjectCords.getY() && y <= endObjectCords.getY()) {
int pixel = texture.getRGB(x - startObjectCords.getX(), y - startObjectCords.getY()); int pixel = texture.getRGB(x - startObjectCords.getX(), y - startObjectCords.getY());
int alpha = (pixel >> 24) & 0xff; var pixelData = getPixelData(pixel);
int r = (pixel >> 16) & 0xff; int r, g, b;
int g = (pixel >> 8) & 0xff;
int b = pixel & 0xff;
if (alpha != 0) { if (pixelData.alpha != 0) {
if (isSelected) { if (isSelected) {
r = Math.min(255, (int) (r * factor)); r = Math.min(255, (int) (pixelData.r * factor));
g = Math.min(255, (int) (g * factor)); g = Math.min(255, (int) (pixelData.g * factor));
b = Math.min(255, (int) (b * factor)); b = Math.min(255, (int) (pixelData.b * factor));
pixel = (alpha << 24) | (r << 16) | (g << 8) | b; pixel = (pixelData.alpha << 24) | (r << 16) | (g << 8) | b;
} }
return new PixelResult(pixel, false); return new PixelResult(pixel, false);
@@ -121,17 +119,15 @@ public class RerenderUtils {
if (x >= startDroppedItemCords.getX() && x <= endDroppedItemCords.getX() && y >= startDroppedItemCords.getY() && y <= endDroppedItemCords.getY()) { if (x >= startDroppedItemCords.getX() && x <= endDroppedItemCords.getX() && y >= startDroppedItemCords.getY() && y <= endDroppedItemCords.getY()) {
int pixel = texture.getRGB(x - startDroppedItemCords.getX(), y - startDroppedItemCords.getY()); int pixel = texture.getRGB(x - startDroppedItemCords.getX(), y - startDroppedItemCords.getY());
int alpha = (pixel >> 24) & 0xff; var pixelData = getPixelData(pixel);
int r = (pixel >> 16) & 0xff; int r, g, b;
int g = (pixel >> 8) & 0xff;
int b = pixel & 0xff;
if (alpha != 0) { if (pixelData.alpha != 0) {
if (isSelected) { if (isSelected) {
r = Math.min(255, (int) (r * factor)); r = Math.min(255, (int) (pixelData.r * factor));
g = Math.min(255, (int) (g * factor)); g = Math.min(255, (int) (pixelData.g * factor));
b = Math.min(255, (int) (b * factor)); b = Math.min(255, (int) (pixelData.b * factor));
pixel = (alpha << 24) | (r << 16) | (g << 8) | b; pixel = (pixelData.alpha << 24) | (r << 16) | (g << 8) | b;
} }
return new PixelResult(pixel, false); return new PixelResult(pixel, false);
@@ -147,17 +143,15 @@ public class RerenderUtils {
RoomCords endObjectCords = new RoomCords(startObjectCords.getX() + texture.getWidth() - 1, startObjectCords.getY() + texture.getHeight() - 1); RoomCords endObjectCords = new RoomCords(startObjectCords.getX() + texture.getWidth() - 1, startObjectCords.getY() + texture.getHeight() - 1);
if (x >= startObjectCords.getX() && x <= endObjectCords.getX() && y >= startObjectCords.getY() && y <= endObjectCords.getY()) { if (x >= startObjectCords.getX() && x <= endObjectCords.getX() && y >= startObjectCords.getY() && y <= endObjectCords.getY()) {
int pixel = texture.getRGB(x - startObjectCords.getX(), y - startObjectCords.getY()); int pixel = texture.getRGB(x - startObjectCords.getX(), y - startObjectCords.getY());
int alpha = (pixel >> 24) & 0xff; var pixelData = getPixelData(pixel);
int r = (pixel >> 16) & 0xff; int r, g, b;
int g = (pixel >> 8) & 0xff;
int b = pixel & 0xff;
if (alpha != 0) { if (pixelData.alpha != 0) {
if (isSelected) { if (isSelected) {
r = Math.min(255, (int) (r * factor)); r = Math.min(255, (int) (pixelData.r * factor));
g = Math.min(255, (int) (g * factor)); g = Math.min(255, (int) (pixelData.g * factor));
b = Math.min(255, (int) (b * factor)); b = Math.min(255, (int) (pixelData.b * factor));
pixel = (alpha << 24) | (r << 16) | (g << 8) | b; pixel = (pixelData.alpha << 24) | (r << 16) | (g << 8) | b;
} }
return new PixelResult(pixel, false); return new PixelResult(pixel, false);
@@ -197,4 +191,16 @@ public class RerenderUtils {
return doorPositions; return doorPositions;
} }
public record ColorData(int r, int g, int b, int alpha) {
}
public static ColorData getPixelData(int pixel) {
int alpha = (pixel >> 24) & 0xff;
int r = (pixel >> 16) & 0xff;
int g = (pixel >> 8) & 0xff;
int b = pixel & 0xff;
return new ColorData(r, g, b, alpha);
}
} }

View File

@@ -0,0 +1,201 @@
package cz.jzitnik.utils;
import com.googlecode.lanterna.TextColor;
import cz.jzitnik.annotations.Dependency;
import cz.jzitnik.annotations.injectors.InjectDependency;
import cz.jzitnik.game.ResourceManager;
import cz.jzitnik.ui.pixels.AlphaPixel;
import cz.jzitnik.ui.pixels.ColoredPixel;
import cz.jzitnik.ui.pixels.Empty;
import java.awt.*;
import java.awt.font.FontRenderContext;
import java.awt.font.GlyphVector;
import java.awt.geom.Rectangle2D;
import java.awt.image.BufferedImage;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
@Dependency
public class TextRenderer {
@InjectDependency
private ResourceManager resourceManager;
public AlphaPixel[][] renderText(String text, int width, int height, Color textColor, float size) {
Font font = loadFont(size);
BufferedImage img = new BufferedImage(width, height, BufferedImage.TYPE_INT_ARGB);
Graphics2D g2d = createGraphics(img, font, textColor);
FontMetrics fm = g2d.getFontMetrics();
List<String> lines = calculateWordWrapping(text, fm, width, height);
int verticalOffset = calculateVerticalOffset(text, font, fm.getFontRenderContext(), lines);
int lineHeight = fm.getHeight();
int y = fm.getAscent() - verticalOffset;
for (String line : lines) {
g2d.drawString(line, 0, y);
y += lineHeight;
}
g2d.dispose();
return convertToPixels(img, width, height);
}
public AlphaPixel[][][] renderTypingAnimation(String text, int width, int height, Color textColor, float size) {
Font font = loadFont(size);
BufferedImage img = new BufferedImage(width, height, BufferedImage.TYPE_INT_ARGB);
Graphics2D g2d = createGraphics(img, font, textColor);
FontMetrics fm = g2d.getFontMetrics();
int lineHeight = fm.getHeight();
int ascent = fm.getAscent();
List<String> lines = calculateWordWrapping(text, fm, width, height);
int verticalOffset = calculateVerticalOffset(text, font, fm.getFontRenderContext(), lines);
int startY = ascent - verticalOffset;
int totalChars = 0;
for (String line : lines) totalChars += line.length();
AlphaPixel[][][] frames = new AlphaPixel[totalChars + 1][height][width];
int frameIndex = 0;
for (int l = 0; l < lines.size(); l++) {
String fullLine = lines.get(l);
for (int c = 0; c < fullLine.length(); c++) {
clearImage(img);
int drawY = startY;
for (int prevL = 0; prevL < l; prevL++) {
g2d.drawString(lines.get(prevL), 0, drawY);
drawY += lineHeight;
}
String partialLine = fullLine.substring(0, c + 1);
g2d.drawString(partialLine, 0, drawY);
frames[frameIndex++] = convertToPixels(img, width, height);
}
}
if (frameIndex < frames.length) {
frames[frameIndex] = frames[frameIndex - 1];
}
g2d.dispose();
return frames;
}
private int calculateVerticalOffset(String text, Font font, FontRenderContext frc, List<String> lines) {
if (lines.isEmpty() || text.isBlank()) return 0;
String firstLine = lines.get(0);
if (firstLine.isBlank() && lines.size() > 1) firstLine = lines.get(1);
if (firstLine.isBlank()) return 0;
GlyphVector gv = font.createGlyphVector(frc, firstLine);
Rectangle2D visualBounds = gv.getVisualBounds();
float ascent = font.getLineMetrics(firstLine, frc).getAscent();
double topPixelPos = ascent + visualBounds.getY();
return (int) topPixelPos;
}
private Font loadFont(float size) {
try {
return Font.createFont(Font.TRUETYPE_FONT, resourceManager.getResourceAsStream("fonts/default.ttf")).deriveFont(size);
} catch (FontFormatException | IOException e) {
throw new RuntimeException("Failed to load font", e);
}
}
private Graphics2D createGraphics(BufferedImage img, Font font, Color color) {
Graphics2D g2d = img.createGraphics();
g2d.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
g2d.setRenderingHint(RenderingHints.KEY_TEXT_ANTIALIASING, RenderingHints.VALUE_TEXT_ANTIALIAS_ON);
g2d.setFont(font);
g2d.setColor(color);
return g2d;
}
private void clearImage(BufferedImage img) {
Graphics2D g = img.createGraphics();
g.setComposite(AlphaComposite.Clear);
g.fillRect(0, 0, img.getWidth(), img.getHeight());
g.dispose();
}
private List<String> calculateWordWrapping(String text, FontMetrics fm, int width, int height) {
List<String> lines = new ArrayList<>();
String[] words = text.split(" ");
StringBuilder line = new StringBuilder();
for (String word : words) {
String testLine = line.isEmpty() ? word : line + " " + word;
if (fm.stringWidth(testLine) > width) {
if (!line.isEmpty()) lines.add(line.toString());
line = new StringBuilder(word);
} else {
line = new StringBuilder(testLine);
}
}
if (!line.isEmpty()) lines.add(line.toString());
int maxLines = height / fm.getHeight();
if (lines.size() > maxLines) {
return lines.subList(0, maxLines);
}
return lines;
}
private AlphaPixel[][] convertToPixels(BufferedImage img, int width, int height) {
AlphaPixel[][] pixels = new AlphaPixel[height][width];
int[] rawPixels = new int[width * height];
img.getRGB(0, 0, width, height, rawPixels, 0, width);
for (int j = 0; j < height; j++) {
for (int i = 0; i < width; i++) {
int argb = rawPixels[j * width + i];
int alpha = (argb >> 24) & 0xFF;
if (alpha == 0) {
pixels[j][i] = new Empty();
} else {
int r = (argb >> 16) & 0xFF;
int g = (argb >> 8) & 0xFF;
int b = argb & 0xFF;
pixels[j][i] = new ColoredPixel(new TextColor.RGB(r, g, b));
}
}
}
return pixels;
}
public Dimension measureText(String text, int maxWidth, float size) {
Font font = loadFont(size);
BufferedImage tmp = new BufferedImage(1, 1, BufferedImage.TYPE_INT_ARGB);
Graphics2D g2d = tmp.createGraphics();
g2d.setFont(font);
FontMetrics fm = g2d.getFontMetrics();
List<String> lines = calculateWordWrapping(text, fm, maxWidth, Integer.MAX_VALUE);
int totalHeight = lines.size() * fm.getHeight();
int calculatedWidth = 0;
for (String line : lines) {
int lineWidth = fm.stringWidth(line);
if (lineWidth > calculatedWidth) {
calculatedWidth = lineWidth;
}
}
g2d.dispose();
return new Dimension(calculatedWidth, totalHeight);
}
}

Binary file not shown.

Binary file not shown.

After

Width:  |  Height:  |  Size: 262 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 138 B