feat: Stamina and sprint

This commit is contained in:
2026-01-02 18:50:10 +01:00
parent f8bc960af2
commit 27cdde97a0
16 changed files with 288 additions and 44 deletions

View File

@@ -2,6 +2,7 @@ package cz.jzitnik;
import cz.jzitnik.game.setup.GameSetup;
import cz.jzitnik.utils.DependencyManager;
import cz.jzitnik.utils.ScheduledTaskManager;
import cz.jzitnik.utils.ThreadManager;
import org.reflections.Reflections;
@@ -13,9 +14,11 @@ public class Game {
GameSetup gameSetup = dependencyManager.getDependencyOrThrow(GameSetup.class);
ThreadManager threadManager = dependencyManager.getDependencyOrThrow(ThreadManager.class);
ScheduledTaskManager scheduledTaskManager = dependencyManager.getDependencyOrThrow(ScheduledTaskManager.class);
gameSetup.setup();
threadManager.startAll();
scheduledTaskManager.startAll();
cli.run();
}

View File

@@ -0,0 +1,14 @@
package cz.jzitnik.annotations;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import java.lang.annotation.ElementType;
import java.util.concurrent.TimeUnit;
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
public @interface ScheduledTask {
long rate();
TimeUnit rateUnit();
}

View File

@@ -1,6 +1,7 @@
package cz.jzitnik.config;
import cz.jzitnik.annotations.Config;
import cz.jzitnik.events.handlers.PlayerMoveEventHandler;
import lombok.Getter;
@Getter
@@ -8,5 +9,10 @@ import lombok.Getter;
public class PlayerConfig {
private final double playerReach = 20;
private final int playerMoveDistance = 3;
private final int playerMoveDistanceSprinting = 6;
private final PlayerMoveEventHandler.SprintKey sprintKey = PlayerMoveEventHandler.SprintKey.CTRL;
private final int swingTimeMs = 500;
private final int staminaIncreaseRateMs = 500;
private final int staminaDelayMs = 1000;
}

View File

@@ -6,6 +6,7 @@ import cz.jzitnik.annotations.injectors.InjectState;
import cz.jzitnik.events.ExitEvent;
import cz.jzitnik.states.RunningState;
import cz.jzitnik.utils.DependencyManager;
import cz.jzitnik.utils.ScheduledTaskManager;
import cz.jzitnik.utils.StateManager;
import cz.jzitnik.utils.ThreadManager;
import cz.jzitnik.utils.events.AbstractEventHandler;
@@ -22,6 +23,9 @@ public class ExitEventHandler extends AbstractEventHandler<ExitEvent> {
@InjectDependency
private RoomTaskScheduler roomTaskScheduler;
@InjectDependency
private ScheduledTaskManager scheduledTaskManager;
public ExitEventHandler(DependencyManager dm) {
super(dm);
}
@@ -29,6 +33,7 @@ public class ExitEventHandler extends AbstractEventHandler<ExitEvent> {
@Override
public void handle(ExitEvent event) {
threadManager.shutdownAll();
scheduledTaskManager.shutdown();
roomTaskScheduler.finalShutdown();
runningState.setRunning(false);
System.exit(0); // Pls don't blame me

View File

@@ -19,6 +19,7 @@ import cz.jzitnik.game.ResourceManager;
import cz.jzitnik.game.utils.RoomCords;
import cz.jzitnik.states.RenderState;
import cz.jzitnik.states.ScreenBuffer;
import cz.jzitnik.states.PlayerMovementState;
import cz.jzitnik.states.TerminalState;
import cz.jzitnik.utils.DependencyManager;
import cz.jzitnik.utils.RerenderUtils;
@@ -31,37 +32,31 @@ import java.awt.image.BufferedImage;
@Slf4j
@EventHandler(PlayerMoveEvent.class)
public class PlayerMoveEventHandler extends AbstractEventHandler<PlayerMoveEvent> {
@InjectState
private GameState gameState;
@InjectDependency
private EventManager eventManager;
@InjectState
private TerminalState terminalState;
@InjectState
private ScreenBuffer screenBuffer;
@InjectDependency
private ResourceManager resourceManager;
@InjectConfig
private Debugging debugging;
@InjectConfig
private PlayerConfig playerConfig;
@InjectState
private RenderState renderState;
@InjectConfig
private Logging logging;
@InjectState
private PlayerMovementState playerMovementState;
public PlayerMoveEventHandler(DependencyManager dm) {
super(dm);
}
@InjectState
private GameState gameState;
@InjectDependency
private EventManager eventManager;
@InjectState
private TerminalState terminalState;
@InjectState
private ScreenBuffer screenBuffer;
@InjectDependency
private ResourceManager resourceManager;
@InjectConfig
private Debugging debugging;
@InjectConfig
private PlayerConfig playerConfig;
@InjectState
private RenderState renderState;
@InjectConfig
private Logging logging;
@Override
public void handle(PlayerMoveEvent event) {
if (renderState.isTerminalTooSmall()) {
@@ -73,7 +68,13 @@ public class PlayerMoveEventHandler extends AbstractEventHandler<PlayerMoveEvent
RoomCords playerCords = player.getPlayerCords();
GameRoom currentRoom = gameState.getCurrentRoom();
int moveStep = playerConfig.getPlayerMoveDistance();
boolean isSprinting = player.getStamina() > 0 && switch (playerConfig.getSprintKey()) {
case CTRL -> event.getKeyStroke().isCtrlDown();
case SHIFT -> event.getKeyStroke().isShiftDown();
case ALT -> event.getKeyStroke().isAltDown();
};
int moveStep = isSprinting ? playerConfig.getPlayerMoveDistanceSprinting() : playerConfig.getPlayerMoveDistance();
int originalPlayerX = playerCords.getX();
int originalPlayerY = playerCords.getY();
@@ -123,6 +124,11 @@ public class PlayerMoveEventHandler extends AbstractEventHandler<PlayerMoveEvent
player.setPlayerRotation(Player.PlayerRotation.RIGHT);
}
}
playerMovementState.setLastMovement(System.currentTimeMillis());
if (isSprinting) {
int newStamina = player.decreaseStamina();
}
int newPlayerX = playerCords.getX();
int newPlayerY = playerCords.getY();
if (logging.isShowPlayerCordsLogs()) {
@@ -148,4 +154,10 @@ public class PlayerMoveEventHandler extends AbstractEventHandler<PlayerMoveEvent
)
));
}
public static enum SprintKey {
CTRL,
SHIFT,
ALT
}
}

View File

@@ -1,6 +1,7 @@
package cz.jzitnik.game;
import cz.jzitnik.game.items.GameItem;
import cz.jzitnik.game.items.types.interfaces.WeaponInterface;
import cz.jzitnik.game.utils.RoomCords;
import cz.jzitnik.ui.Inventory;
import lombok.Getter;
@@ -16,13 +17,37 @@ import java.util.concurrent.TimeUnit;
@Getter
@Slf4j
public class Player {
public static final int MAX_STAMINA = 20;
private final RoomCords playerCords;
private final ScheduledExecutorService scheduler = Executors.newScheduledThreadPool(1);
private final GameItem[] inventory = new GameItem[Inventory.ITEMS_X * Inventory.ITEMS_Y];
private int stamina = MAX_STAMINA;
@Setter
private GameItem selectedItem;
private boolean swinging = false;
public int increaseStamina() {
log.debug("Stamina: {}", stamina + 1);
return ++stamina;
}
public int decreaseStamina() {
log.debug("Stamina: {}", stamina - 1);
return --stamina;
}
public int getDamageDeal() {
int damage = 1;
// Probably in the future, there will be more logic like potions, etc.
log.debug("Selected item: {}", selectedItem);
if (selectedItem.getType() instanceof WeaponInterface item) {
damage = item.getDamageDeal();
}
return damage;
}
public boolean addItem(GameItem item) {
boolean added = false;
for (int i = 0; i < inventory.length; i++) {

View File

@@ -2,7 +2,7 @@ package cz.jzitnik.game.items.types;
import cz.jzitnik.game.items.strategy.Strategy;
public sealed interface ItemType<T> permits Sword {
public interface ItemType<T> {
Class<T> getItemType();
Strategy getStrategy();
}

View File

@@ -1,19 +1,15 @@
package cz.jzitnik.game.items.types;
import cz.jzitnik.game.items.strategy.Strategy;
import cz.jzitnik.game.items.strategy.SwordStrategy;
import lombok.AllArgsConstructor;
import lombok.Getter;
@AllArgsConstructor
@Getter
public non-sealed class Sword implements ItemType<Sword> {
protected int damageDeal;
public final Class<Sword> getItemType() {
return Sword.class;
public class Sword extends Weapon {
public Sword(int damageDeal) {
super(damageDeal);
}
public SwordStrategy getStrategy() {
@Override
public Strategy getStrategy() {
return new SwordStrategy(this);
}
}

View File

@@ -0,0 +1,19 @@
package cz.jzitnik.game.items.types;
import cz.jzitnik.game.items.strategy.Strategy;
import cz.jzitnik.game.items.types.interfaces.WeaponInterface;
import lombok.AllArgsConstructor;
import lombok.Getter;
@AllArgsConstructor
@Getter
public abstract class Weapon implements ItemType<Weapon>, WeaponInterface {
protected int damageDeal;
@Override
public final Class<Weapon> getItemType() {
return Weapon.class;
}
public abstract Strategy getStrategy();
}

View File

@@ -0,0 +1,5 @@
package cz.jzitnik.game.items.types.interfaces;
public interface WeaponInterface {
int getDamageDeal();
}

View File

@@ -61,8 +61,7 @@ public abstract class HittableMob extends Mob {
public void interact(DependencyManager dm) {
dm.inject(this);
// TODO: Swords in hand will deal more damage, for now deal always one
health--;
health -= gameState.getPlayer().getDamageDeal();
log.debug("Health: {}", health);

View File

@@ -1,13 +1,14 @@
package cz.jzitnik.game.items;
package cz.jzitnik.game.setup.items;
import cz.jzitnik.game.ResourceManager;
import cz.jzitnik.game.items.GameItem;
import cz.jzitnik.game.items.types.Sword;
public class WoodenSword extends GameItem {
public WoodenSword(ResourceManager resourceManager) {
super(
"Wooden sword",
new Sword(5),
new Sword(2),
resourceManager.getResource(ResourceManager.Resource.WOODEN_SWORD)
);
}

View File

@@ -4,7 +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.items.WoodenSword;
import cz.jzitnik.game.setup.items.WoodenSword;
import cz.jzitnik.game.objects.Chest;
import cz.jzitnik.game.setup.mobs.Zombie;
import cz.jzitnik.game.utils.RoomCords;

View File

@@ -0,0 +1,13 @@
package cz.jzitnik.states;
import cz.jzitnik.annotations.State;
import lombok.Data;
import java.util.concurrent.ScheduledFuture;
@Data
@State
public class PlayerMovementState {
long lastMovement;
ScheduledFuture<?> staminaIncreaseSchedule;
}

View File

@@ -0,0 +1,73 @@
package cz.jzitnik.tasks;
import cz.jzitnik.annotations.ScheduledTask;
import cz.jzitnik.annotations.injectors.InjectConfig;
import cz.jzitnik.annotations.injectors.InjectDependency;
import cz.jzitnik.annotations.injectors.InjectState;
import cz.jzitnik.config.PlayerConfig;
import cz.jzitnik.game.GameState;
import cz.jzitnik.game.Player;
import cz.jzitnik.states.PlayerMovementState;
import cz.jzitnik.utils.DependencyManager;
import cz.jzitnik.utils.ScheduledTaskManager;
import java.util.concurrent.ScheduledFuture;
import java.util.concurrent.TimeUnit;
@ScheduledTask(rate = 1, rateUnit = TimeUnit.SECONDS)
public class StaminaIncreaseTask implements Runnable {
@InjectState
private PlayerMovementState playerMovementState;
@InjectState
private GameState gameState;
@InjectDependency
private ScheduledTaskManager scheduledTaskManager;
@InjectDependency
private DependencyManager dependencyManager;
@InjectConfig
private PlayerConfig playerConfig;
@Override
public void run() {
if (playerMovementState.getStaminaIncreaseSchedule() != null) {
return;
}
long nowTime = System.currentTimeMillis();
if (nowTime - playerMovementState.getLastMovement() >= playerConfig.getStaminaDelayMs() && gameState.getPlayer().getStamina() < Player.MAX_STAMINA) {
IncreaseStamina instance = new IncreaseStamina();
dependencyManager.inject(instance);
playerMovementState.setStaminaIncreaseSchedule(scheduledTaskManager.tempScheduleFixedRate(instance, playerConfig.getStaminaIncreaseRateMs(), TimeUnit.MILLISECONDS));
}
}
public static class IncreaseStamina implements Runnable {
@InjectState
private PlayerMovementState playerMovementState;
@InjectState
private GameState gameState;
@InjectConfig
private PlayerConfig playerConfig;
@Override
public void run() {
long nowTime = System.currentTimeMillis();
Player player = gameState.getPlayer();
if (player.getStamina() >= Player.MAX_STAMINA || nowTime - playerMovementState.getLastMovement() < playerConfig.getStaminaDelayMs()) {
ScheduledFuture<?> future = playerMovementState.getStaminaIncreaseSchedule();
playerMovementState.setStaminaIncreaseSchedule(null);
future.cancel(false);
return;
}
player.increaseStamina();
}
}
}

View File

@@ -0,0 +1,73 @@
package cz.jzitnik.utils;
import cz.jzitnik.annotations.Dependency;
import cz.jzitnik.annotations.ScheduledTask;
import cz.jzitnik.config.ThreadPoolConfig;
import lombok.extern.slf4j.Slf4j;
import org.reflections.Reflections;
import java.lang.reflect.InvocationTargetException;
import java.util.HashSet;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.ScheduledFuture;
import java.util.concurrent.TimeUnit;
@Slf4j
@Dependency
public class ScheduledTaskManager {
private final ScheduledExecutorService scheduler = Executors.newScheduledThreadPool(new ThreadPoolConfig().getTaskThreadCount());
private final HashSet<Registry> instances = new HashSet<>();
private final DependencyManager dependencyManager;
public ScheduledTaskManager(Reflections reflections, DependencyManager dependencyManager) {
this.dependencyManager = dependencyManager;
var classes = reflections.getTypesAnnotatedWith(ScheduledTask.class);
for (Class<?> clazz : classes) {
if (!Runnable.class.isAssignableFrom(clazz)) {
continue;
}
try {
var instance = (Runnable) clazz.getDeclaredConstructor().newInstance();
var annotation = clazz.getAnnotation(ScheduledTask.class);
assert annotation != null;
instances.add(new Registry(instance, annotation));
} catch (InstantiationException | IllegalAccessException | InvocationTargetException |
NoSuchMethodException e) {
throw new RuntimeException(e);
}
}
}
public void startAll() {
for (Registry instance : instances) {
dependencyManager.inject(instance.runnable);
scheduler.scheduleAtFixedRate(instance.runnable, 0, instance.scheduledTask.rate(), instance.scheduledTask.rateUnit());
}
}
public ScheduledFuture<?> tempScheduleFixedRate(Runnable runnable, int rate, TimeUnit rateUnit) {
return scheduler.scheduleAtFixedRate(runnable, 0, rate, rateUnit);
}
public void shutdown() {
try {
scheduler.shutdown();
if (!scheduler.awaitTermination(1, TimeUnit.SECONDS)) {
scheduler.shutdownNow();
if (!scheduler.awaitTermination(1, TimeUnit.SECONDS)) {
log.error("Pool did not terminate");
}
}
} catch (InterruptedException e) {
scheduler.shutdownNow();
Thread.currentThread().interrupt();
}
}
private record Registry(Runnable runnable, ScheduledTask scheduledTask) {
}
}