feat: Implemented text rendering
This commit is contained in:
@@ -9,6 +9,8 @@ import cz.jzitnik.game.Constants;
|
||||
import cz.jzitnik.states.RenderState;
|
||||
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.ui.pixels.Pixel;
|
||||
import cz.jzitnik.utils.DependencyManager;
|
||||
@@ -41,6 +43,7 @@ public class CliHandler extends AbstractEventHandler<RerenderScreen> {
|
||||
|
||||
var parts = event.parts();
|
||||
var buffer = screenBuffer.getRenderedBuffer();
|
||||
var globalOverrideBuffer = screenBuffer.getGlobalOverrideBuffer();
|
||||
var terminalScreen = terminalState.getTerminalScreen();
|
||||
var tg = terminalState.getTextGraphics();
|
||||
|
||||
@@ -53,9 +56,9 @@ public class CliHandler extends AbstractEventHandler<RerenderScreen> {
|
||||
for (int y = startYNormalized; y <= endYNormalized; y += 2) {
|
||||
for (int x = start.getColumn(); x <= end.getColumn(); x++) {
|
||||
try {
|
||||
Pixel topPixel = buffer[y][x];
|
||||
Pixel topPixel = getPixel(buffer[y][x], globalOverrideBuffer[y][x]);
|
||||
Pixel bottomPixel = (y + 1 <= end.getRow())
|
||||
? buffer[y + 1][x]
|
||||
? getPixel(buffer[y + 1][x], globalOverrideBuffer[y + 1][x])
|
||||
: new 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,
|
||||
TextColor topColor,
|
||||
TextColor bottomColor) {
|
||||
|
||||
160
src/main/java/cz/jzitnik/events/handlers/DialogEventHandler.java
Normal file
160
src/main/java/cz/jzitnik/events/handlers/DialogEventHandler.java
Normal file
@@ -0,0 +1,160 @@
|
||||
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.GameState;
|
||||
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.ui.pixels.Pixel;
|
||||
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 MARGIN_BOTTOM = 15;
|
||||
private static final int PADDING = 7;
|
||||
private static final int BUTTON_TEXT_PADDING = 4;
|
||||
private static final int QUESTION_ACTIONS_GAP = 10;
|
||||
private static final int BUTTON_HEIGHT = 15;
|
||||
private static final int BUTTON_PADDING = 5;
|
||||
private static final float FONT_SIZE = 15f;
|
||||
|
||||
public static TerminalSize getSize(TextRenderer textRenderer, Dialog dialog) {
|
||||
int WIDTH = 355;
|
||||
var textSize = textRenderer.measureText(dialog.getText(), WIDTH - PADDING * 2, FONT_SIZE);
|
||||
|
||||
return new TerminalSize(355, PADDING + textSize.height + (
|
||||
dialog.getOnEnd() instanceof OnEnd.AskQuestion(
|
||||
OnEnd.AskQuestion.Answer[] answers
|
||||
) ? answers.length * BUTTON_HEIGHT + (answers.length - 1) * BUTTON_PADDING : 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) {
|
||||
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));
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
for (AlphaPixel[][] buf : animation) {
|
||||
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 (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), 1f);
|
||||
} 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());
|
||||
}
|
||||
|
||||
if (onEnd instanceof OnEnd.Continue(Dialog nextDialog)) {
|
||||
Thread.sleep(1000);
|
||||
eventManager.emitEvent(nextDialog);
|
||||
} else if (onEnd instanceof OnEnd.AskQuestion(OnEnd.AskQuestion.Answer[] answers)) {
|
||||
|
||||
}
|
||||
} catch (InterruptedException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -76,7 +76,10 @@ public class MouseActionEventHandler extends AbstractEventHandler<MouseAction> {
|
||||
|
||||
gameState.getPlayer().swing(playerConfig.getSwingTimeMs());
|
||||
|
||||
object.ifPresent(selectable -> selectable.interact(dm));
|
||||
object.ifPresent(selectable -> {
|
||||
dm.inject(selectable);
|
||||
selectable.interact();
|
||||
});
|
||||
}
|
||||
default -> uiRoomClickHandlerRepository.handleElse(event);
|
||||
}
|
||||
|
||||
@@ -4,9 +4,9 @@ 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.GameState;
|
||||
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;
|
||||
@@ -18,17 +18,17 @@ public class QuestionAnswerEventHandler extends AbstractEventHandler<QuestionAns
|
||||
}
|
||||
|
||||
@InjectState
|
||||
private GameState gameState;
|
||||
private DialogState dialogState;
|
||||
|
||||
@InjectDependency
|
||||
private EventManager eventManager;
|
||||
|
||||
@Override
|
||||
public void handle(QuestionAnswerEvent event) {
|
||||
OnEnd dialogOnEnd = gameState.getCurrentDialog().getOnEnd();
|
||||
OnEnd dialogOnEnd = dialogState.getCurrentDialog().getOnEnd();
|
||||
|
||||
if (dialogOnEnd instanceof OnEnd.AskQuestion dialog) {
|
||||
OnEnd.AskQuestion.Answer answer = dialog.getAnswers()[event.getQuestionIndex()];
|
||||
OnEnd.AskQuestion.Answer answer = dialog.answers()[event.getQuestionIndex()];
|
||||
Dialog switchTo = answer.dialog();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,6 +8,7 @@ import cz.jzitnik.events.FullRoomDraw;
|
||||
import cz.jzitnik.events.TerminalResizeEvent;
|
||||
import cz.jzitnik.game.GameState;
|
||||
import cz.jzitnik.states.ScreenBuffer;
|
||||
import cz.jzitnik.ui.pixels.AlphaPixel;
|
||||
import cz.jzitnik.ui.pixels.Empty;
|
||||
import cz.jzitnik.ui.pixels.Pixel;
|
||||
import cz.jzitnik.utils.DependencyManager;
|
||||
@@ -40,12 +41,15 @@ public class TerminalResizeEventHandler extends AbstractEventHandler<TerminalRes
|
||||
int height = size.getRows() * 2;
|
||||
|
||||
Pixel[][] buffer = new Pixel[height][width];
|
||||
AlphaPixel[][] globalOverride = new AlphaPixel[height][width];
|
||||
for (int x = 0; x < width; x++) {
|
||||
for (int y = 0; y < height; y++) {
|
||||
buffer[y][x] = new Empty();
|
||||
globalOverride[y][x] = new Empty();
|
||||
}
|
||||
}
|
||||
screenBuffer.setRenderedBuffer(buffer);
|
||||
screenBuffer.setGlobalOverrideBuffer(globalOverride);
|
||||
|
||||
if (gameState.getScreen() != null) {
|
||||
if (screenRerendering) {
|
||||
|
||||
@@ -26,10 +26,6 @@ public class GameState {
|
||||
@Setter
|
||||
private Interactable interacting;
|
||||
|
||||
@Getter
|
||||
@Setter
|
||||
private Dialog currentDialog;
|
||||
|
||||
@Getter
|
||||
private Screen screen;
|
||||
|
||||
|
||||
@@ -1,24 +1,10 @@
|
||||
package cz.jzitnik.game.dialog;
|
||||
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Getter;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
|
||||
public interface OnEnd {
|
||||
record Continue(Dialog nextDialog) implements OnEnd {
|
||||
}
|
||||
|
||||
@Getter
|
||||
@RequiredArgsConstructor
|
||||
@AllArgsConstructor
|
||||
class AskQuestion implements OnEnd {
|
||||
private final String question;
|
||||
private final Answer[] answers;
|
||||
/**
|
||||
* Characters per second
|
||||
*/
|
||||
private int typingSpeed = 10;
|
||||
|
||||
record AskQuestion(Answer[] answers) implements OnEnd {
|
||||
public record Answer(String answer, Dialog dialog) {
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,12 +1,17 @@
|
||||
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.utils.DependencyManager;
|
||||
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;
|
||||
|
||||
@@ -20,8 +25,17 @@ public abstract class DialogMob extends Mob {
|
||||
this.dialog = dialog;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void interact(DependencyManager dm) {
|
||||
@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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -73,9 +73,7 @@ public abstract class HittableMob extends Mob {
|
||||
}
|
||||
|
||||
@Override
|
||||
public final void interact(DependencyManager dm) {
|
||||
dm.inject(this);
|
||||
|
||||
public final void interact() {
|
||||
health -= gameState.getPlayer().getDamageDeal();
|
||||
|
||||
log.debug("Health: {}", health);
|
||||
|
||||
@@ -72,8 +72,7 @@ public final class Chest extends GameObject implements UIClickHandler {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void interact(DependencyManager dm) {
|
||||
dm.inject(this);
|
||||
public void interact() {
|
||||
log.debug("Interacted with chest");
|
||||
render(false);
|
||||
}
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
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.InventoryRerender;
|
||||
import cz.jzitnik.game.GameRoom;
|
||||
@@ -31,18 +33,19 @@ public final class DroppedItem implements Selectable, Serializable {
|
||||
return item.getTexture();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void interact(DependencyManager dm) {
|
||||
StateManager stateManager = dm.getDependencyOrThrow(StateManager.class);
|
||||
GameState gameState = stateManager.getOrThrow(GameState.class);
|
||||
@InjectState
|
||||
private GameState gameState;
|
||||
|
||||
@InjectDependency
|
||||
private EventManager eventManager;
|
||||
|
||||
@Override
|
||||
public void interact() {
|
||||
if (!gameState.getPlayer().addItem(item)) {
|
||||
return;
|
||||
}
|
||||
|
||||
EventManager eventManager = dm.getDependencyOrThrow(EventManager.class);
|
||||
var currentRoom = gameState.getCurrentRoom();
|
||||
currentRoom.getDroppedItems().remove(this);
|
||||
gameState.getCurrentRoom().getDroppedItems().remove(this);
|
||||
eventManager.emitEvent(new InventoryRerender());
|
||||
eventManager.emitEvent(new DroppedItemRerender(this));
|
||||
}
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
package cz.jzitnik.game.objects;
|
||||
|
||||
import cz.jzitnik.utils.DependencyManager;
|
||||
|
||||
public interface Interactable {
|
||||
void interact(DependencyManager dm);
|
||||
void interact();
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
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;
|
||||
@@ -9,18 +10,17 @@ import cz.jzitnik.utils.roomtasks.RoomTask;
|
||||
import java.awt.image.BufferedImage;
|
||||
|
||||
public class Pepa extends DialogMob {
|
||||
public Pepa(BufferedImage texture, RoomTask[] tasks, RoomCords cords) {
|
||||
super(texture, tasks, cords,
|
||||
public Pepa(ResourceManager resourceManager, RoomCords cords) {
|
||||
super(resourceManager.getResource(ResourceManager.Resource.PLAYER_FRONT), new RoomTask[]{}, cords,
|
||||
new Dialog(
|
||||
"Never gonna give you up",
|
||||
"Pepa: Never gonna give you up",
|
||||
new OnEnd.Continue(new Dialog(
|
||||
"Never gonna let you down",
|
||||
"Pepa: Never gonna let you down",
|
||||
new OnEnd.Continue(new Dialog(
|
||||
"Never gonna run around", new OnEnd.AskQuestion(
|
||||
"How it continues?",
|
||||
"Pepa: How it continues?", new OnEnd.AskQuestion(
|
||||
new OnEnd.AskQuestion.Answer[]{
|
||||
new OnEnd.AskQuestion.Answer("And desert you", new Dialog("You are god damn right!", new OnEnd.Continue(null))),
|
||||
new OnEnd.AskQuestion.Answer("You are a dessert", new Dialog("WRONG!", new OnEnd.Continue(null)))
|
||||
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)))
|
||||
}
|
||||
)))
|
||||
))
|
||||
|
||||
@@ -4,6 +4,7 @@ import cz.jzitnik.game.GameRoom;
|
||||
import cz.jzitnik.game.GameRoomPart;
|
||||
import cz.jzitnik.game.ResourceManager;
|
||||
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.WoodenSword;
|
||||
import cz.jzitnik.game.objects.Chest;
|
||||
@@ -26,7 +27,9 @@ public class MainRoom extends GameRoom {
|
||||
));
|
||||
addObject(chest);
|
||||
|
||||
Zombie zombie = new Zombie(resourceManager, new RoomCords(100, 100));
|
||||
addMob(zombie);
|
||||
//Zombie zombie = new Zombie(resourceManager, new RoomCords(100, 100));
|
||||
//addMob(zombie);
|
||||
Pepa pepa = new Pepa(resourceManager, new RoomCords(100, 100));
|
||||
addMob(pepa);
|
||||
}
|
||||
}
|
||||
|
||||
11
src/main/java/cz/jzitnik/states/DialogState.java
Normal file
11
src/main/java/cz/jzitnik/states/DialogState.java
Normal file
@@ -0,0 +1,11 @@
|
||||
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;
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
package cz.jzitnik.states;
|
||||
|
||||
import cz.jzitnik.annotations.State;
|
||||
import cz.jzitnik.ui.pixels.AlphaPixel;
|
||||
import cz.jzitnik.ui.pixels.Pixel;
|
||||
import lombok.Data;
|
||||
|
||||
@@ -8,4 +9,5 @@ import lombok.Data;
|
||||
@State
|
||||
public class ScreenBuffer {
|
||||
private Pixel[][] renderedBuffer = new Pixel[][] {};
|
||||
private AlphaPixel[][] globalOverrideBuffer = new AlphaPixel[][] {};
|
||||
}
|
||||
|
||||
76
src/main/java/cz/jzitnik/ui/DialogUI.java
Normal file
76
src/main/java/cz/jzitnik/ui/DialogUI.java
Normal file
@@ -0,0 +1,76 @@
|
||||
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.states.DialogState;
|
||||
import cz.jzitnik.states.TerminalState;
|
||||
import cz.jzitnik.utils.TextRenderer;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
@Slf4j
|
||||
@UI
|
||||
@Dependency
|
||||
public class DialogUI {
|
||||
@InjectState
|
||||
private DialogState dialogState;
|
||||
|
||||
@InjectState
|
||||
private TerminalState terminalState;
|
||||
|
||||
@InjectDependency
|
||||
private TextRenderer textRenderer;
|
||||
|
||||
@MouseHandler(MouseHandlerType.CLICK)
|
||||
public boolean handleClick(MouseAction mouseAction) {
|
||||
if (dialogState.getCurrentDialog() == null) {
|
||||
return false;
|
||||
}
|
||||
TerminalSize size = DialogEventHandler.getSize(textRenderer, dialogState.getCurrentDialog());
|
||||
TerminalPosition start = DialogEventHandler.getStart(terminalState.getTerminalScreen().getTerminalSize(), size);
|
||||
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)) {
|
||||
return false;
|
||||
}
|
||||
TerminalPosition localPosition = new TerminalPosition(mouseNormalized.getColumn() - start.getColumn(), mouseNormalized.getRow() - start.getRow());
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
@MouseHandler(MouseHandlerType.MOVE)
|
||||
public boolean handleMove(MouseAction mouseAction) {
|
||||
if (dialogState.getCurrentDialog() == null) {
|
||||
return false;
|
||||
}
|
||||
TerminalSize size = DialogEventHandler.getSize(textRenderer, dialogState.getCurrentDialog());
|
||||
TerminalPosition start = DialogEventHandler.getStart(terminalState.getTerminalScreen().getTerminalSize(), size);
|
||||
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)) {
|
||||
return false;
|
||||
}
|
||||
TerminalPosition localPosition = new TerminalPosition(mouseNormalized.getColumn() - start.getColumn(), mouseNormalized.getRow() - start.getRow());
|
||||
|
||||
log.debug("Position: {}", localPosition);
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
13
src/main/java/cz/jzitnik/ui/pixels/AlphaPixel.java
Normal file
13
src/main/java/cz/jzitnik/ui/pixels/AlphaPixel.java
Normal 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;
|
||||
}
|
||||
}
|
||||
@@ -2,8 +2,12 @@ package cz.jzitnik.ui.pixels;
|
||||
|
||||
import com.googlecode.lanterna.TextColor;
|
||||
|
||||
public final class ColoredPixel extends Pixel {
|
||||
public final class ColoredPixel extends AlphaPixel {
|
||||
public ColoredPixel(TextColor color) {
|
||||
super(color);
|
||||
super(color, 1f);
|
||||
}
|
||||
|
||||
public ColoredPixel(TextColor color, float alpha) {
|
||||
super(color, alpha);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
package cz.jzitnik.ui.pixels;
|
||||
|
||||
public final class Empty extends Pixel {
|
||||
public final class Empty extends AlphaPixel {
|
||||
public Empty() {
|
||||
super(null);
|
||||
super(null, 0f);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,6 +6,6 @@ import lombok.Getter;
|
||||
|
||||
@Getter
|
||||
@AllArgsConstructor
|
||||
public sealed abstract class Pixel permits Empty, ColoredPixel {
|
||||
public sealed abstract class Pixel permits AlphaPixel {
|
||||
protected TextColor color;
|
||||
}
|
||||
|
||||
201
src/main/java/cz/jzitnik/utils/TextRenderer.java
Normal file
201
src/main/java/cz/jzitnik/utils/TextRenderer.java
Normal 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);
|
||||
}
|
||||
}
|
||||
BIN
src/main/resources/fonts/default.ttf
Normal file
BIN
src/main/resources/fonts/default.ttf
Normal file
Binary file not shown.
Reference in New Issue
Block a user