chore: Idk some changes that I made

This commit is contained in:
2025-05-05 10:57:07 +02:00
parent 2e2f4b00f7
commit d09209848e
18 changed files with 424 additions and 26 deletions

View File

@ -34,8 +34,17 @@ import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.CopyOnWriteArrayList;
/**
* Main class representing the game world and core logic.
* <p>
* Manages the game state, player actions, world updates and interactions.
*/
@Getter
public class Game {
/**
* The 2D world grid consisting of block lists for each coordinate.
* Dimensions: [height][width] = [256][512].
*/
@SuppressWarnings("unchecked")
private final List<Block>[][] world = (List<Block>[][]) new CopyOnWriteArrayList[256][512];
private final Player player = new Player();
@ -45,15 +54,25 @@ public class Game {
private final Inventory inventory = new Inventory();
private transient EntitySpawnProvider entitySpawnProvider = new EntitySpawnProvider();
private transient GameStates gameStates = new GameStates(this);
/** Current time of day in the game (0600 range). */
@Setter
private int daytime = 0; // 0-600
private int daytime = 0;
//
private Configuration configuration = new Configuration();
/**
* Constructs the game instance and generates the initial world.
*/
public Game() {
Generation.generateWorld(this);
}
/**
* Returns the current coordinates of the player (bottom half).
*
* @return int array with x and y coordinates, or null if not found.
*/
public int[] getPlayerCords() {
for (int i = 0; i < world.length; i++) {
for (int j = 0; j < world[i].length; j++) {
@ -73,6 +92,12 @@ public class Game {
return null;
}
/**
* Moves the player one block to the right if possible.
*
* @param screenRenderer the renderer used to refresh the screen.
* @param terminal the terminal instance used for layout.
*/
public void movePlayerRight(ScreenRenderer screenRenderer, Terminal terminal) {
if (window != Window.WORLD) {
return;
@ -96,6 +121,12 @@ public class Game {
update(screenRenderer);
}
/**
* Moves the player one block to the left if possible.
*
* @param screenRenderer the renderer used to refresh the screen.
* @param terminal the terminal instance used for layout.
*/
public void movePlayerLeft(ScreenRenderer screenRenderer, Terminal terminal) {
if (window != Window.WORLD) {
return;
@ -119,6 +150,11 @@ public class Game {
update(screenRenderer);
}
/**
* Moves the player upward by one block, if jumping is valid.
*
* @param screenRenderer the renderer used to refresh the screen.
*/
public void movePlayerUp(ScreenRenderer screenRenderer) {
if (window != Window.WORLD) {
return;
@ -145,6 +181,13 @@ public class Game {
}).start();
}
/**
* Attacks mobs at a specified location.
*
* @param screenRenderer the renderer used to refresh the screen.
* @param x x-coordinate.
* @param y y-coordinate.
*/
public void hit(ScreenRenderer screenRenderer, int x, int y) {
if (mining || window != Window.WORLD) {
return;
@ -182,6 +225,13 @@ public class Game {
}).start();
}
/**
* Initiates the mining process at a given location.
*
* @param screenRenderer the renderer used to refresh the screen.
* @param x x-coordinate.
* @param y y-coordinate.
*/
public void mine(ScreenRenderer screenRenderer, int x, int y) {
if (mining || window != Window.WORLD) {
return;
@ -250,6 +300,14 @@ public class Game {
}).start();
}
/**
* Instantly mines a block without animation delay.
*
* @param screenRenderer the renderer used to refresh the screen.
* @param x x-coordinate.
* @param y y-coordinate.
* @param minedDirectly whether the block was mined directly by player.
*/
public void mineInstant(ScreenRenderer screenRenderer, int x, int y, boolean minedDirectly) {
var blocks = world[y][x];
var blocksCopy = new ArrayList<>(blocks);
@ -316,6 +374,14 @@ public class Game {
update(screenRenderer);
}
/**
* Checks whether a block is valid for mining based on proximity and visibility.
*
* @param x x-coordinate.
* @param y y-coordinate.
* @param terminal terminal context used to determine screen bounds.
* @return true if mineable, false otherwise.
*/
public boolean isMineable(int x, int y, Terminal terminal) {
List<Block> blocks = world[y][x];
@ -340,6 +406,15 @@ public class Game {
&& blocks.stream().anyMatch(Block::isMineable);
}
/**
* Checks whether a block is valid for hitting (attacking).
*
* @param x x-coordinate.
* @param y y-coordinate.
* @param terminal terminal context used to determine screen bounds.
* @return true if a mob can be hit at the given location.
*/
public boolean isHitable(int x, int y, Terminal terminal) {
List<Block> blocks = world[y][x];
@ -364,6 +439,11 @@ public class Game {
&& blocks.stream().anyMatch(Block::isMob);
}
/**
* Periodically checks and updates the players falling state due to gravity.
*
* @param screenRenderer the renderer used to refresh the screen.
*/
public void update(ScreenRenderer screenRenderer) {
while (true) {
try {
@ -395,6 +475,14 @@ public class Game {
}
}
/**
* Attempts to place a block or interact with the environment at a given location.
* Also handles eating and tool usage.
*
* @param x x-coordinate.
* @param y y-coordinate.
* @param screenRenderer the renderer used to refresh the screen.
*/
public void build(int x, int y, ScreenRenderer screenRenderer) {
if (window != Window.WORLD) {
return;
@ -476,6 +564,12 @@ public class Game {
}
}
/**
* Switches the selected inventory slot (hotbar).
*
* @param slot the slot index to switch to.
* @param screenRenderer the renderer used to refresh the screen.
*/
public void changeSlot(int slot, ScreenRenderer screenRenderer) {
if (window != Window.WORLD) {
return;
@ -485,10 +579,21 @@ public class Game {
screenRenderer.render(this);
}
/**
* Checks if a block list represents a solid space (i.e., not all ghost blocks).
*
* @param blocks list of blocks at a coordinate.
* @return true if any block is not a ghost block.
*/
public boolean isSolid(List<Block> blocks) {
return !blocks.stream().allMatch(Block::isGhost);
}
/**
* Visually and logically triggers the player "hit" (damage) animation.
*
* @param screenRenderer the renderer used to refresh the screen.
*/
public void playerHit(ScreenRenderer screenRenderer) {
player.getPlayerBlock1().setSpriteState(SteveState.FIRST_HURT);
player.getPlayerBlock2().setSpriteState(SteveState.SECOND_HURT);

View File

@ -76,6 +76,7 @@ public class SpriteLoader {
PIG,
SHEEP,
COW,
ZOMBIE,
// UI
BREAKING,
@ -267,6 +268,7 @@ public class SpriteLoader {
SPRITES_MAP.put(SPRITES.PIG, new Pig());
SPRITES_MAP.put(SPRITES.SHEEP, new Sheep());
SPRITES_MAP.put(SPRITES.COW, new Cow());
SPRITES_MAP.put(SPRITES.ZOMBIE, new Zombie());
// UI
SPRITES_MAP.put(SPRITES.BREAKING, new Breaking());

View File

@ -3,7 +3,7 @@ package cz.jzitnik.game.core.sound.registry;
import cz.jzitnik.game.annotations.SoundRegistry;
import cz.jzitnik.game.core.sound.SoundKey;
@SoundRegistry(key = SoundKey.STONE_WALKING, resourceLocation = {
@SoundRegistry(key = SoundKey.WOOD_WALKING, resourceLocation = {
"wood/step1.ogg",
"wood/step2.ogg",
"wood/step3.ogg",

View File

@ -11,6 +11,7 @@ import cz.jzitnik.game.mobs.EntityHurtAnimation;
import cz.jzitnik.game.mobs.EntityKill;
import cz.jzitnik.game.smelting.Smelting;
import cz.jzitnik.game.sprites.ui.Font;
import cz.jzitnik.game.ui.DeathScreen;
import cz.jzitnik.game.ui.Escape;
import cz.jzitnik.game.ui.Options;
@ -27,6 +28,7 @@ public class Dependencies {
public Font font = new Font();
public Escape escape;
public Options options;
public DeathScreen deathScreen = new DeathScreen();
public Dependencies(Game game) {
escape = new Escape(game);

View File

@ -10,6 +10,7 @@ import cz.jzitnik.game.Game;
import cz.jzitnik.game.annotations.ReduceFallDamage;
import cz.jzitnik.game.core.reducefalldamage.Reducer;
import cz.jzitnik.game.core.sound.SoundKey;
import cz.jzitnik.game.ui.Window;
import cz.jzitnik.tui.ScreenRenderer;
@Getter
@ -41,18 +42,19 @@ public class Player {
}
public void fell(List<Block> fallblock, Game game, ScreenRenderer screenRenderer) {
var block = fallblock.stream().filter(b -> b.getClass().isAnnotationPresent(ReduceFallDamage.class)).findFirst();
var block = fallblock.stream().filter(b -> b.getClass().isAnnotationPresent(ReduceFallDamage.class))
.findFirst();
int damage = Math.max(fallDistance - 3, 0);
if (block.isPresent()) {
var reducerClass = block.get().getClass().getAnnotation(ReduceFallDamage.class).value();
try {
Reducer reducer = reducerClass.getDeclaredConstructor().newInstance();
Reducer reducer = reducerClass.getDeclaredConstructor().newInstance();
damage = reducer.reduce(damage);
} catch (InstantiationException | IllegalAccessException | IllegalArgumentException
| InvocationTargetException | NoSuchMethodException | SecurityException e) {
e.printStackTrace();
} catch (InstantiationException | IllegalAccessException | IllegalArgumentException
| InvocationTargetException | NoSuchMethodException | SecurityException e) {
e.printStackTrace();
System.exit(0);
}
}
}
dealDamage(damage, game, screenRenderer);
fallDistance = 0;
@ -65,8 +67,9 @@ public class Player {
game.getGameStates().dependencies.sound.playSound(game.getConfiguration(), SoundKey.HURT, null);
}
if (health == 0) {
// TODO: Implement dead
if (health <= 0) {
game.setWindow(Window.DEATH_SCREEN);
screenRenderer.render(game);
}
}

View File

@ -0,0 +1,19 @@
package cz.jzitnik.game.entities.items.registry.mobs;
import cz.jzitnik.game.SpriteLoader;
import cz.jzitnik.game.annotations.EntityRegistry;
import cz.jzitnik.game.entities.Block;
import cz.jzitnik.game.mobs.services.zombie.ZombieData;
@EntityRegistry("zombie")
public class Zombie extends Block {
public Zombie() {
super("zombie", SpriteLoader.SPRITES.ZOMBIE);
setMob(true);
setGhost(true);
setSpriteState(cz.jzitnik.game.sprites.Zombie.ZombieState.TOP);
setMineable(false);
setData(new ZombieData());
setHp(10);
}
}

View File

@ -1,10 +1,12 @@
package cz.jzitnik.game.mobs.services.sheep;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
@Getter
@Setter
@NoArgsConstructor
public class SheepData {
private int lastDirection = 1; // 1 = right, -1 = left
private int movementCooldown = 0;

View File

@ -0,0 +1,12 @@
package cz.jzitnik.game.mobs.services.zombie;
import lombok.Getter;
import lombok.Setter;
@Getter
@Setter
public class ZombieData {
private int lastDirection = 1; // 1 = right, -1 = left
private int movementCooldown = 0;
private int jumpAttempts = 0;
}

View File

@ -0,0 +1,117 @@
package cz.jzitnik.game.mobs.services.zombie;
import cz.jzitnik.game.Game;
import cz.jzitnik.game.annotations.EntityHurtAnimationHandler;
import cz.jzitnik.game.annotations.EntityKillHandler;
import cz.jzitnik.game.annotations.EntityLogic;
import cz.jzitnik.game.annotations.EntitySpawn;
import cz.jzitnik.game.entities.Block;
import cz.jzitnik.game.entities.items.InventoryItem;
import cz.jzitnik.game.entities.items.ItemBlockSupplier;
import cz.jzitnik.game.mobs.*;
import cz.jzitnik.game.sprites.Zombie;
import cz.jzitnik.game.sprites.Zombie.ZombieState;
import cz.jzitnik.tui.ScreenMovingCalculationProvider;
import org.jline.terminal.Terminal;
import java.util.*;
import static cz.jzitnik.game.sprites.Zombie.ZombieState.*;
@EntitySpawn
@EntityLogic("zombie")
@EntityHurtAnimationHandler("zombie")
@EntityKillHandler("zombie")
public class ZombieLogic
implements EntityLogicInterface, EntitySpawnInterface, EntityHurtAnimationChanger, EntityKillInterface {
private final Random random = new Random();
@Override
public void nextIteration(EntityLogicProvider.EntityLogicMobDTO entityLogicMobDTO) {
}
@Override
public void spawn(int playerX, int playerY, Game game, Terminal terminal) {
int[] view = ScreenMovingCalculationProvider.calculate(playerX, playerY, terminal.getHeight(), terminal.getWidth(), game.getWorld()[0].length, game.getWorld().length);
var world = game.getWorld();
int startX = view[0];
int endX = view[1];
attemptZombieSpawn(startX - 20, startX - 5, playerY, game);
attemptZombieSpawn(endX + 5, endX + 20, playerY, game);
}
private void attemptZombieSpawn(int startX, int endX, int centerY, Game game) {
if (countZombies(startX, endX, centerY - 15, centerY + 15, game) < 3 && random.nextInt(100) < 2) {
var spawnLocations = zombieCanSpawn(startX, endX, centerY, game);
if (!spawnLocations.isEmpty()) {
for (int i = 0; i < Math.min(4, spawnLocations.size()); i++) {
var loc = getRandomEntry(spawnLocations);
int x = loc.getKey();
int y = loc.getValue();
var top = ItemBlockSupplier.getEntity("zombie");
top.setSpriteState(ZombieState.TOP);
var bottom = ItemBlockSupplier.getEntity("zombie");
bottom.setSpriteState(ZombieState.BOTTOM);
game.getWorld()[y - 1][x].add(top);
game.getWorld()[y][x].add(bottom);
}
}
}
}
private HashMap<Integer, Integer> zombieCanSpawn(int startX, int endX, int playerY, Game game) {
var map = new HashMap<Integer, Integer>();
var world = game.getWorld();
for (int x = startX; x <= endX; x++) {
for (int y = Math.max(1, playerY - 30); y < Math.min(world.length - 2, playerY + 30); y++) {
if (world[y + 1][x].stream().anyMatch(b -> b.getBlockId().equals("grass"))) {
map.put(x, y);
}
}
}
return map;
}
private long countZombies(int startX, int endX, int startY, int endY, Game game) {
long count = 0;
for (int y = startY; y <= endY; y++) {
for (int x = startX; x <= endX; x++) {
count += game.getWorld()[y][x].stream().filter(i -> i.getBlockId().equals("zombie")).count();
}
}
return count / 2; // Each zombie has two parts
}
public Zombie.ZombieState setHurtAnimation(boolean hurt, Enum current) {
if (hurt) {
return switch (current) {
case TOP, TOP_HURT -> TOP_HURT;
case BOTTOM, BOTTOM_HURT -> BOTTOM_HURT;
default -> throw new IllegalStateException("Unexpected state: " + current);
};
}
return switch (current) {
case TOP, TOP_HURT -> TOP;
case BOTTOM, BOTTOM_HURT -> BOTTOM;
default -> throw new IllegalStateException("Unexpected state: " + current);
};
}
@Override
public void killed(Game game, Block mob) {
int rottenFlesh = random.nextInt(3) + 1;
InventoryItem drop = new InventoryItem(rottenFlesh, ItemBlockSupplier.getItem("rotten_flesh"));
game.getInventory().addItem(drop);
}
public static <K, V> Map.Entry<K, V> getRandomEntry(HashMap<K, V> map) {
List<Map.Entry<K, V>> list = new ArrayList<>(map.entrySet());
return list.get(new Random().nextInt(list.size()));
}
}

View File

@ -0,0 +1,33 @@
package cz.jzitnik.game.sprites;
import java.util.HashMap;
import cz.jzitnik.tui.Sprite;
public class Zombie extends Sprite<Zombie.ZombieState> {
public enum ZombieState {
TOP, TOP_HURT,
BOTTOM, BOTTOM_HURT
}
public Zombie() {
loadResources(new HashMap<>() {
{
put(ZombieState.TOP, "mobs/zombie/top.ans");
put(ZombieState.TOP_HURT, "mobs/zombie/tophurt.ans");
put(ZombieState.BOTTOM, "mobs/zombie/bottom.ans");
put(ZombieState.BOTTOM_HURT, "mobs/zombie/bottomhurt.ans");
}
}, ZombieState.class);
}
@Override
public String getSprite() {
return getSprite(ZombieState.TOP);
}
@Override
public String getSprite(ZombieState key) {
return super.getResource(key);
}
}

View File

@ -235,7 +235,6 @@ public class Font {
public FontDTO line(Terminal terminal, String text, Object... attributes) {
int termWidth = terminal.getWidth();
// int termHeight = terminal.getHeight();
Size fontSize = Size.MEDIUM;
Align alignment = Align.LEFT;

View File

@ -19,20 +19,18 @@ public class InputHandlerThread extends Thread {
private final Terminal terminal;
private final ScreenRenderer screenRenderer;
private final MouseHandler mouseHandler;
private final boolean[] isRunning;
public InputHandlerThread(Game game, Terminal terminal, ScreenRenderer screenRenderer, boolean[] isRunning) {
public InputHandlerThread(Game game, Terminal terminal, ScreenRenderer screenRenderer) {
this.game = game;
this.terminal = terminal;
this.screenRenderer = screenRenderer;
this.mouseHandler = new MouseHandler(game, terminal, screenRenderer);
this.isRunning = isRunning;
}
@Override
public void run() {
try {
while (isRunning[0]) {
while (true) {
int key = terminal.reader().read();
if (key == 27) { // ESC
@ -46,19 +44,25 @@ public class InputHandlerThread extends Thread {
case INVENTORY -> InventoryClickHandler.click(mouseEvent, terminal, screenRenderer,
game, Optional.empty(), Optional.empty());
case CRAFTING_TABLE ->
game.getGameStates().craftingTable.click(mouseEvent, terminal, screenRenderer);
game.getGameStates().craftingTable.click(mouseEvent, terminal, screenRenderer);
case CHEST -> ((Chest) game.getWorld()[game.getGameStates().clickY][game
.getGameStates().clickX].stream().filter(i -> i.getBlockId().equals("chest"))
.toList().getFirst().getData()).click(game, mouseEvent, terminal,
screenRenderer);
.toList().getFirst().getData()).click(game, mouseEvent, terminal,
screenRenderer);
case FURNACE -> ((Furnace) game.getWorld()[game.getGameStates().clickY][game
.getGameStates().clickX].stream().filter(i -> i.getBlockId().equals("furnace"))
.toList().getFirst().getData()).click(game, mouseEvent, terminal,
screenRenderer);
case ESC -> game.getGameStates().dependencies.escape.mouse(mouseEvent, terminal, screenRenderer);
case SAVE_EXIT -> {}
case OPTIONS -> game.getGameStates().dependencies.options.handleMouse(mouseEvent, terminal, screenRenderer);
case SAVED -> {}
.toList().getFirst().getData()).click(game, mouseEvent, terminal,
screenRenderer);
case ESC -> game.getGameStates().dependencies.escape.mouse(mouseEvent, terminal,
screenRenderer);
case SAVE_EXIT -> {
}
case OPTIONS -> game.getGameStates().dependencies.options.handleMouse(mouseEvent,
terminal, screenRenderer);
case DEATH_SCREEN -> game.getGameStates().dependencies.deathScreen
.handleMouse(mouseEvent, terminal, screenRenderer, game);
case SAVED -> {
}
}
}
}

View File

@ -0,0 +1,99 @@
package cz.jzitnik.game.ui;
import org.jline.terminal.MouseEvent;
import org.jline.terminal.Terminal;
import cz.jzitnik.game.Game;
import cz.jzitnik.game.sprites.ui.Font;
import cz.jzitnik.game.sprites.ui.Font.Align;
import cz.jzitnik.game.sprites.ui.Font.Background;
import cz.jzitnik.game.sprites.ui.Font.Custom;
import cz.jzitnik.game.sprites.ui.Font.Size;
import cz.jzitnik.tui.ScreenRenderer;
import lombok.extern.slf4j.Slf4j;
@Slf4j
public class DeathScreen {
private int btnWidth;
private int leftPad;
private int mainTextHeight;
private int buttonHeight;
private int textButtonMargin;
private boolean buttonHover;
public void render(StringBuilder buffer, Terminal terminal, Game game) {
log.debug("Rendering death screen");
var font = game.getGameStates().dependencies.font;
var width = terminal.getWidth();
var height = terminal.getHeight();
var heading = font.line(terminal, "You Died", Size.LARGE, Align.CENTER);
buffer.append(heading.getData());
textButtonMargin = (height < 600) ? 15 : (height < 800) ? 30 : 50;
buffer.append("\n".repeat(textButtonMargin));
renderButton(buffer, "Respawn", width, font, terminal, buttonHover);
}
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;
log.debug("Button width: {}px ", btnWidth);
final String color = selected ? "[48;2;70;70;70m" : "[48;2;116;115;113m";
var text = font.line(terminal, txt, Size.SMALL, Align.CENTER, new Custom.Width(btnWidth - 4),
new Background(color));
buttonHeight = text.getHeight() + 4;
var lines = text.getData().split("\n");
for (int y = 0; y < buttonHeight; y++) {
buffer.append(" ".repeat(leftPad));
if (y < 2 || y >= buttonHeight - 2) {
buffer.append("\033").append(color).append(" ".repeat(btnWidth));
} else {
buffer.append("\033").append(color).append(" ".repeat(2));
buffer.append(lines[y - 2]);
buffer.append("\033").append(color).append(" ".repeat(2));
}
buffer.append("\033[0m\n");
}
}
public void handleMouse(MouseEvent mouseEvent, Terminal terminal, ScreenRenderer screenRenderer, Game game) {
int x = mouseEvent.getX();
int y = mouseEvent.getY();
var type = mouseEvent.getType();
int buttonx = x - leftPad;
int buttony = y - (mainTextHeight + textButtonMargin) - 2;
int margin = textButtonMargin / 2;
boolean changed = false;
if (buttonx > 0 && buttonx <= btnWidth) {
int top = (buttonHeight + margin);
int bottom = top + buttonHeight;
if (buttony > top && buttony < bottom) {
if (type == MouseEvent.Type.Pressed) {
game.setWindow(Window.WORLD);
return;
}
if (!buttonHover) {
buttonHover = true;
changed = true;
}
} else if (buttonHover) {
buttonHover = false;
changed = true;
}
}
if (changed) {
screenRenderer.render(game);
}
}
}

View File

@ -1,5 +1,5 @@
package cz.jzitnik.game.ui;
public enum Window {
WORLD, INVENTORY, CRAFTING_TABLE, CHEST, FURNACE, ESC, SAVE_EXIT, SAVED, OPTIONS
WORLD, INVENTORY, CRAFTING_TABLE, CHEST, FURNACE, ESC, SAVE_EXIT, SAVED, OPTIONS, DEATH_SCREEN,
}

View File

@ -84,6 +84,7 @@ public class ScreenRenderer {
case OPTIONS -> game.getGameStates().dependencies.options.render(main, terminal);
case SAVE_EXIT -> game.getGameStates().dependencies.escape.renderSave(main, terminal);
case SAVED -> game.getGameStates().dependencies.escape.renderSaved(main, terminal);
case DEATH_SCREEN -> game.getGameStates().dependencies.deathScreen.render(main, terminal, game);
case WORLD -> {
// World

View File

@ -86,7 +86,7 @@ public class Menu {
}
}
private void renderButton(StringBuilder buffer, String txt, int width, Font font, Terminal terminal, boolean selected) {
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;
log.debug("Button width: {}px ", btnWidth);