Compare commits

...

2 Commits

Author SHA1 Message Date
75393f2bea feat: Pausing the game and many other changes 2025-09-24 21:52:47 +02:00
579bc05538 fix: Jumping infinitely deals damage 2025-09-24 21:14:07 +02:00
17 changed files with 152 additions and 29 deletions

View File

@ -1,6 +1,7 @@
package cz.jzitnik; package cz.jzitnik;
import cz.jzitnik.game.GameSaver; import cz.jzitnik.game.GameSaver;
import cz.jzitnik.game.context.list.GameLoopContext;
import cz.jzitnik.game.logic.CustomLogicProvider; import cz.jzitnik.game.logic.CustomLogicProvider;
import cz.jzitnik.game.mobs.EntityLogicProvider; import cz.jzitnik.game.mobs.EntityLogicProvider;
import cz.jzitnik.game.threads.ThreadProvider; import cz.jzitnik.game.threads.ThreadProvider;
@ -38,15 +39,21 @@ public class Main {
var gameSaver = new GameSaver(); var gameSaver = new GameSaver();
var game = gameSaver.load(); var game = gameSaver.load();
final boolean[] isRunning = { true }; game.registerContext(Terminal.class, terminal);
GameLoopContext gameLoopContext = game.getContext(GameLoopContext.class);
EntityLogicProvider entityLogicProvider = new EntityLogicProvider(); EntityLogicProvider entityLogicProvider = new EntityLogicProvider();
CustomLogicProvider customLogicProvider = new CustomLogicProvider(); CustomLogicProvider customLogicProvider = new CustomLogicProvider();
ThreadProvider threadProvider = new ThreadProvider(game, screenRenderer, terminal, isRunning); ThreadProvider threadProvider = new ThreadProvider(game, screenRenderer, terminal);
threadProvider.start(); threadProvider.start();
while (isRunning[0]) { while (true) {
if (!gameLoopContext.isRunning()) {
Thread.sleep(1000);
continue;
}
try { try {
entityLogicProvider.update(game); entityLogicProvider.update(game);
} catch (Exception ignored) { } catch (Exception ignored) {
@ -64,10 +71,6 @@ public class Main {
Thread.sleep(1000); Thread.sleep(1000);
} }
log.info("Closing terminal");
terminal.trackMouse(Terminal.MouseTracking.Off);
terminal.close();
} catch (IOException | InterruptedException ignored) { } catch (IOException | InterruptedException ignored) {
} }
} }

View File

@ -51,7 +51,6 @@ public class Game {
private final List<Block>[][] world = (List<Block>[][]) new CopyOnWriteArrayList[256][512]; private final List<Block>[][] world = (List<Block>[][]) new CopyOnWriteArrayList[256][512];
private final Player player = new Player(); private final Player player = new Player();
private transient boolean mining = false; private transient boolean mining = false;
@Setter
private transient Window window = Window.WORLD; private transient Window window = Window.WORLD;
private final Inventory inventory = new Inventory(); private final Inventory inventory = new Inventory();
private transient EntitySpawnProvider entitySpawnProvider = new EntitySpawnProvider(); private transient EntitySpawnProvider entitySpawnProvider = new EntitySpawnProvider();
@ -73,10 +72,15 @@ public class Game {
Generation.generateWorld(this); Generation.generateWorld(this);
} }
public boolean isNight(){ public boolean isNight() {
return daytime > 200 && daytime < 400; return daytime > 200 && daytime < 400;
} }
public void setWindow(Window window) {
this.window = window;
gameStates.dependencies.windowSwitchHandlerProvider.handle(this);
}
/** /**
* Returns the current coordinates of the player (bottom half). * Returns the current coordinates of the player (bottom half).
* *
@ -169,12 +173,19 @@ public class Game {
update(screenRenderer); update(screenRenderer);
} }
private boolean playerMovingUp = false;
/** /**
* Moves the player upward by one block, if jumping is valid. * Moves the player upward by one block, if jumping is valid.
* *
* @param screenRenderer the renderer used to refresh the screen. * @param screenRenderer the renderer used to refresh the screen.
*/ */
public void movePlayerUp(ScreenRenderer screenRenderer) { public void movePlayerUp(ScreenRenderer screenRenderer) {
if (playerMovingUp) {
return;
}
playerMovingUp = true;
if (window != Window.WORLD) { if (window != Window.WORLD) {
return; return;
} }
@ -191,6 +202,8 @@ public class Game {
stats.setBlocksTraveled(stats.getBlocksTraveled() + 1); stats.setBlocksTraveled(stats.getBlocksTraveled() + 1);
screenRenderer.render(this);
new Thread(() -> { new Thread(() -> {
try { try {
Thread.sleep(400); Thread.sleep(400);
@ -199,6 +212,7 @@ public class Game {
} }
update(screenRenderer); update(screenRenderer);
playerMovingUp = false;
}).start(); }).start();
} }
@ -657,7 +671,11 @@ public class Game {
} }
} }
public Object getContext(Class clazz) { public <T> T getContext(Class<T> clazz) {
return gameStates.globalContextProvider.getMap(clazz); return gameStates.globalContextProvider.getMap(clazz);
} }
public void registerContext(Class clazz, Object instance) {
gameStates.globalContextProvider.registerContext(clazz, instance);
}
} }

View File

@ -0,0 +1,11 @@
package cz.jzitnik.game.annotations;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import java.lang.annotation.ElementType;
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
public @interface WindowSwitchHandler {
}

View File

@ -17,7 +17,7 @@ public class Bed implements RightClickHandler {
@Override @Override
public void onBlockRightClick(int x, int y, Game game, ScreenRenderer screenRenderer) { public void onBlockRightClick(int x, int y, Game game, ScreenRenderer screenRenderer) {
BedSleepGlobalContext bedSleepGlobalContext = BedSleepGlobalContext bedSleepGlobalContext =
(BedSleepGlobalContext) game.getContext(BedSleepGlobalContext.class); game.getContext(BedSleepGlobalContext.class);
if (bedSleepGlobalContext.isSleeping() || game.getDaytime() < 200 || game.getDaytime() > 500) { if (bedSleepGlobalContext.isSleeping() || game.getDaytime() < 200 || game.getDaytime() > 500) {
return; // already in progress return; // already in progress

View File

@ -11,7 +11,8 @@ public class GlobalContextProvider {
private final Map<Class, Object> data = new HashMap<>(); private final Map<Class, Object> data = new HashMap<>();
private boolean loaded = false; private boolean loaded = false;
public Object getMap(Class<?> clazz) { @SuppressWarnings("unchecked")
public <T> T getMap(Class<T> clazz) {
if (!loaded) { if (!loaded) {
Reflections reflections = new Reflections("cz.jzitnik.game.context"); Reflections reflections = new Reflections("cz.jzitnik.game.context");
Set<Class<?>> handlerClasses = reflections.getTypesAnnotatedWith(GlobalContext.class); Set<Class<?>> handlerClasses = reflections.getTypesAnnotatedWith(GlobalContext.class);
@ -28,6 +29,10 @@ public class GlobalContextProvider {
loaded = true; loaded = true;
} }
return data.get(clazz); return clazz.cast(data.get(clazz));
}
public void registerContext(Class clazz, Object instance) {
data.put(clazz, instance);
} }
} }

View File

@ -0,0 +1,10 @@
package cz.jzitnik.game.context.list;
import cz.jzitnik.game.annotations.GlobalContext;
import lombok.Data;
@Data
@GlobalContext
public class GameLoopContext {
private boolean isRunning = true;
}

View File

@ -7,6 +7,7 @@ import cz.jzitnik.game.handlers.events.EventHandlerProvider;
import cz.jzitnik.game.handlers.pickup.PickupHandlerProvider; import cz.jzitnik.game.handlers.pickup.PickupHandlerProvider;
import cz.jzitnik.game.handlers.place.PlaceHandler; import cz.jzitnik.game.handlers.place.PlaceHandler;
import cz.jzitnik.game.handlers.tooluse.ToolUseProvider; import cz.jzitnik.game.handlers.tooluse.ToolUseProvider;
import cz.jzitnik.game.handlers.windowswitch.WindowSwitchHandlerProvider;
import cz.jzitnik.game.mobs.EntityHurtAnimation; import cz.jzitnik.game.mobs.EntityHurtAnimation;
import cz.jzitnik.game.mobs.EntityKill; import cz.jzitnik.game.mobs.EntityKill;
import cz.jzitnik.game.smelting.Smelting; import cz.jzitnik.game.smelting.Smelting;
@ -20,6 +21,7 @@ public class Dependencies {
public EntityHurtAnimation entityHurtAnimation = new EntityHurtAnimation(); public EntityHurtAnimation entityHurtAnimation = new EntityHurtAnimation();
public EntityKill entityKill = new EntityKill(); public EntityKill entityKill = new EntityKill();
public PickupHandlerProvider pickupHandlerProvider = new PickupHandlerProvider(); public PickupHandlerProvider pickupHandlerProvider = new PickupHandlerProvider();
public WindowSwitchHandlerProvider windowSwitchHandlerProvider = new WindowSwitchHandlerProvider();
public GameSaver gameSaver = new GameSaver(); public GameSaver gameSaver = new GameSaver();
public EventHandlerProvider eventHandlerProvider = new EventHandlerProvider(); public EventHandlerProvider eventHandlerProvider = new EventHandlerProvider();
public Smelting smelting = new Smelting(); public Smelting smelting = new Smelting();

View File

@ -1,5 +1,6 @@
package cz.jzitnik.game.entities; package cz.jzitnik.game.entities;
import cz.jzitnik.game.context.list.GameLoopContext;
import lombok.Getter; import lombok.Getter;
import lombok.Setter; import lombok.Setter;

View File

@ -0,0 +1,7 @@
package cz.jzitnik.game.handlers.windowswitch;
import cz.jzitnik.game.Game;
public interface WindowSwitchHandlerInterface {
void handle(Game game);
}

View File

@ -0,0 +1,39 @@
package cz.jzitnik.game.handlers.windowswitch;
import java.util.ArrayList;
import java.util.List;
import java.util.Set;
import cz.jzitnik.game.Game;
import cz.jzitnik.game.annotations.WindowSwitchHandler;
import org.reflections.Reflections;
public class WindowSwitchHandlerProvider {
public final List<WindowSwitchHandlerInterface> handler = new ArrayList<>();
public WindowSwitchHandlerProvider() {
registerHandlers();
}
public void handle(Game game) {
handler.forEach(handler -> handler.handle(game));
}
private void registerHandlers() {
Reflections reflections = new Reflections("cz.jzitnik.game.handlers.windowswitch.handlers");
Set<Class<?>> handlerClasses = reflections.getTypesAnnotatedWith(WindowSwitchHandler.class);
for (Class<?> clazz : handlerClasses) {
if (WindowSwitchHandlerInterface.class.isAssignableFrom(clazz)) {
try {
WindowSwitchHandlerInterface handlerInstance = (WindowSwitchHandlerInterface) clazz.getDeclaredConstructor()
.newInstance();
handler.add(handlerInstance);
} catch (Exception e) {
e.printStackTrace();
}
}
}
}
}

View File

@ -0,0 +1,20 @@
package cz.jzitnik.game.handlers.windowswitch.handlers;
import cz.jzitnik.game.Game;
import cz.jzitnik.game.annotations.WindowSwitchHandler;
import cz.jzitnik.game.context.list.GameLoopContext;
import cz.jzitnik.game.handlers.windowswitch.WindowSwitchHandlerInterface;
import cz.jzitnik.game.ui.Window;
import java.util.Arrays;
@WindowSwitchHandler
public class GamePauseHandler implements WindowSwitchHandlerInterface {
private final Window[] pausedWindows = {Window.ESC, Window.DEATH_SCREEN, Window.OPTIONS, Window.SAVE_EXIT, Window.SAVED};
@Override
public void handle(Game game) {
GameLoopContext gameLoopContext = game.getContext(GameLoopContext.class);
gameLoopContext.setRunning(!Arrays.asList(pausedWindows).contains(game.getWindow()));
}
}

View File

@ -1,4 +0,0 @@
package cz.jzitnik.game.logic.services.burning;
public class MobsBurning {
}

View File

@ -9,6 +9,7 @@ import cz.jzitnik.game.annotations.Flamable;
import cz.jzitnik.game.entities.Block; import cz.jzitnik.game.entities.Block;
import cz.jzitnik.game.entities.items.Item; import cz.jzitnik.game.entities.items.Item;
import cz.jzitnik.game.logic.CustomLogicInterface; import cz.jzitnik.game.logic.CustomLogicInterface;
import cz.jzitnik.game.sprites.Steve;
import cz.jzitnik.tui.ScreenRenderer; import cz.jzitnik.tui.ScreenRenderer;
@CustomLogic @CustomLogic
@ -59,7 +60,7 @@ public class LavaFireLogic implements CustomLogicInterface {
block.setOnFire(true); block.setOnFire(true);
block.setBurningTime(0); block.setBurningTime(0);
} }
} else if (!block.getClass().isAnnotationPresent(FireImmune.class)) { } else if (!block.getClass().isAnnotationPresent(FireImmune.class) && !block.getBlockId().equals("steve")) {
int dealDamage = game.getInventory().getItemInHand().map(Item::getDealDamage).orElse(1); int dealDamage = game.getInventory().getItemInHand().map(Item::getDealDamage).orElse(1);
if (block.getHp() - dealDamage <= 0) { if (block.getHp() - dealDamage <= 0) {
// Mob is killed // Mob is killed

View File

@ -22,7 +22,6 @@ public class ThreadProvider {
private final Game game; private final Game game;
private final ScreenRenderer screenRenderer; private final ScreenRenderer screenRenderer;
private final Terminal terminal; private final Terminal terminal;
private final boolean[] isRunning;
private final List<Thread> list = new ArrayList<>(); private final List<Thread> list = new ArrayList<>();
/** /**
@ -43,11 +42,10 @@ public class ThreadProvider {
* @param terminal The terminal. * @param terminal The terminal.
* @param isRunning A shared boolean array for controlling thread execution. * @param isRunning A shared boolean array for controlling thread execution.
*/ */
public ThreadProvider(Game game, ScreenRenderer screenRenderer, Terminal terminal, boolean[] isRunning) { public ThreadProvider(Game game, ScreenRenderer screenRenderer, Terminal terminal) {
this.game = game; this.game = game;
this.screenRenderer = screenRenderer; this.screenRenderer = screenRenderer;
this.terminal = terminal; this.terminal = terminal;
this.isRunning = isRunning;
registerHandlers(); registerHandlers();
} }
@ -77,8 +75,6 @@ public class ThreadProvider {
params[i] = screenRenderer; params[i] = screenRenderer;
else if (type == Terminal.class) else if (type == Terminal.class)
params[i] = terminal; params[i] = terminal;
else if (type == boolean[].class)
params[i] = isRunning;
else if (type == Player.class) else if (type == Player.class)
params[i] = game.getPlayer(); params[i] = game.getPlayer();
else { else {

View File

@ -88,7 +88,6 @@ public class InputHandlerThread extends Thread {
case 'd' -> new Thread(() -> game.movePlayerRight(screenRenderer, terminal)).start(); case 'd' -> new Thread(() -> game.movePlayerRight(screenRenderer, terminal)).start();
case ' ' ->{ case ' ' ->{
game.movePlayerUp(screenRenderer); game.movePlayerUp(screenRenderer);
screenRenderer.render(game);
} }
case 'e' -> { case 'e' -> {
if (game.getWindow() != Window.WORLD) { if (game.getWindow() != Window.WORLD) {
@ -103,12 +102,13 @@ public class InputHandlerThread extends Thread {
case 'q' -> { case 'q' -> {
if (game.getWindow() != Window.WORLD) { if (game.getWindow() != Window.WORLD) {
game.setWindow(Window.WORLD); game.setWindow(Window.WORLD);
screenRenderer.render(game);
} else { } else {
game.setWindow(Window.ESC); game.setWindow(Window.ESC);
game.getGameStates().dependencies.escape.reset(); game.getGameStates().dependencies.escape.reset();
}
screenRenderer.render(game); screenRenderer.render(game);
} }
}
default -> { default -> {
} }
} }

View File

@ -7,6 +7,8 @@ import cz.jzitnik.game.sprites.ui.Font.*;
import cz.jzitnik.tui.ScreenRenderer; import cz.jzitnik.tui.ScreenRenderer;
import cz.jzitnik.tui.utils.Menu; import cz.jzitnik.tui.utils.Menu;
import java.io.IOException;
/** /**
* Handles the in-game escape menu functionality, including rendering options, * Handles the in-game escape menu functionality, including rendering options,
* handling mouse input, and managing save and exit logic. * handling mouse input, and managing save and exit logic.
@ -22,7 +24,7 @@ public class Escape {
*/ */
public Escape(Game game) { public Escape(Game game) {
this.game = game; this.game = game;
this.menu = new Menu(game, "2DCraft", new String[] { "Continue", "Options", "Save and exit" }, this.menu = new Menu(game, "2DCraft", new String[]{"Continue", "Options", "Save and exit"},
this::onButtonClick); this::onButtonClick);
} }
@ -74,6 +76,15 @@ public class Escape {
game.getGameStates().dependencies.gameSaver.save(game); game.getGameStates().dependencies.gameSaver.save(game);
game.setWindow(Window.SAVED); game.setWindow(Window.SAVED);
screenRenderer.render(game); screenRenderer.render(game);
Terminal terminal = game.getContext(Terminal.class);
terminal.trackMouse(Terminal.MouseTracking.Off);
try {
terminal.close();
} catch (IOException e) {
throw new RuntimeException(e);
}
System.exit(0); System.exit(0);
break; break;
} }

View File

@ -8,6 +8,7 @@ import cz.jzitnik.game.sprites.SimpleSprite;
import cz.jzitnik.game.blocks.Chest; import cz.jzitnik.game.blocks.Chest;
import cz.jzitnik.game.blocks.Furnace; import cz.jzitnik.game.blocks.Furnace;
import cz.jzitnik.game.ui.Healthbar; import cz.jzitnik.game.ui.Healthbar;
import cz.jzitnik.game.ui.Window;
import cz.jzitnik.tui.utils.SpriteCombiner; import cz.jzitnik.tui.utils.SpriteCombiner;
import lombok.Getter; import lombok.Getter;
import lombok.Setter; import lombok.Setter;
@ -29,6 +30,7 @@ public class ScreenRenderer {
private final SpriteList spriteList; private final SpriteList spriteList;
private final Terminal terminal; private final Terminal terminal;
private boolean rendering = false; private boolean rendering = false;
private Window currentlyRenderingWindow;
/** /**
* Constructs a {@code ScreenRenderer} with the given sprite list and terminal. * Constructs a {@code ScreenRenderer} with the given sprite list and terminal.
@ -100,10 +102,11 @@ public class ScreenRenderer {
* @param game Current game state to render. * @param game Current game state to render.
*/ */
public void render(Game game) { public void render(Game game) {
if (rendering) { if (rendering && game.getWindow() == currentlyRenderingWindow) {
return; return;
} }
rendering = true; rendering = true;
currentlyRenderingWindow = game.getWindow();
log.debug("Rendering frame"); log.debug("Rendering frame");
var world = game.getWorld(); var world = game.getWorld();
StringBuilder main = new StringBuilder(); StringBuilder main = new StringBuilder();