feat: Swinging mechanic

This commit is contained in:
2026-01-01 14:23:21 +01:00
parent d9f7f5a2ac
commit ba26b633af
16 changed files with 189 additions and 48 deletions

View File

@@ -8,4 +8,5 @@ import lombok.Getter;
public class PlayerConfig {
private final double playerReach = 20;
private final int playerMoveDistance = 3;
private final int swingTimeMs = 500;
}

View File

@@ -1,13 +1,16 @@
package cz.jzitnik.events.handlers;
import cz.jzitnik.annotations.EventHandler;
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.events.MouseAction;
import cz.jzitnik.events.MouseMoveEvent;
import cz.jzitnik.game.GameState;
import cz.jzitnik.game.objects.GameObject;
import cz.jzitnik.game.objects.Interactable;
import cz.jzitnik.game.utils.Selectable;
import cz.jzitnik.states.RenderState;
import cz.jzitnik.utils.DependencyManager;
import cz.jzitnik.utils.UIClickHandlerRepository;
@@ -15,6 +18,7 @@ import cz.jzitnik.utils.events.AbstractEventHandler;
import cz.jzitnik.utils.events.EventManager;
import java.util.Optional;
import java.util.stream.Stream;
@EventHandler(MouseAction.class)
public class MouseActionEventHandler extends AbstractEventHandler<MouseAction> {
@@ -34,6 +38,9 @@ public class MouseActionEventHandler extends AbstractEventHandler<MouseAction> {
@InjectState
private RenderState renderState;
@InjectConfig
private PlayerConfig playerConfig;
@Override
public void handle(MouseAction event) {
if (gameState.getScreen() != null) {
@@ -48,14 +55,22 @@ public class MouseActionEventHandler extends AbstractEventHandler<MouseAction> {
switch (event.getActionType()) {
case MOVE -> eventManager.emitEvent(new MouseMoveEvent(event));
case CLICK_RELEASE -> {
Optional<GameObject> object = gameState.getCurrentRoom().getObjects().stream().filter(GameObject::isSelected).findFirst();
boolean clicked = uiClickHandlerRepository.handleClick(event);
if (object.isPresent()) {
((Interactable) object.get()).interact(dm);
if (clicked || gameState.getPlayer().isSwinging()) {
return;
}
uiClickHandlerRepository.handleClick(event);
Stream<? extends Selectable> combined = Stream.concat(
gameState.getCurrentRoom().getMobs().stream(),
gameState.getCurrentRoom().getObjects().stream()
);
Optional<? extends Selectable> object = combined.filter(Selectable::isSelected).findFirst();
gameState.getPlayer().swing(playerConfig.getSwingTimeMs());
object.ifPresent(selectable -> selectable.interact(dm));
}
}
}

View File

@@ -15,7 +15,9 @@ import cz.jzitnik.game.GameState;
import cz.jzitnik.game.Player;
import cz.jzitnik.game.ResourceManager;
import cz.jzitnik.game.objects.GameObject;
import cz.jzitnik.game.utils.Renderable;
import cz.jzitnik.game.utils.RoomCords;
import cz.jzitnik.game.utils.Selectable;
import cz.jzitnik.states.ScreenBuffer;
import cz.jzitnik.states.TerminalState;
import cz.jzitnik.utils.DependencyManager;
@@ -30,6 +32,7 @@ import java.util.HashSet;
import java.util.List;
import java.util.Set;
import java.util.stream.Collectors;
import java.util.stream.Stream;
@Slf4j
@EventHandler(MouseMoveEvent.class)
@@ -90,7 +93,10 @@ public class MouseMoveEventHandler extends AbstractEventHandler<MouseMoveEvent>
int startX = start.getX();
int startY = start.getY();
Set<GameObject> selectedObjects = currentRoom.getObjects().stream().filter(gameObject -> {
List<? extends Selectable> combinedObjects = Stream
.concat(currentRoom.getObjects().stream(), currentRoom.getMobs().stream()).toList();
Set<Selectable> selectedObjects = combinedObjects.stream().filter(gameObject -> {
if (!gameObject.isSelectable()) return false;
BufferedImage texture = gameObject.getTexture();
RoomCords cords = gameObject.getCords();
@@ -122,9 +128,9 @@ public class MouseMoveEventHandler extends AbstractEventHandler<MouseMoveEvent>
relativeMouseY < cords.getY() + texture.getHeight();
}).collect(Collectors.toSet());
Set<GameObject> changedObjects = new HashSet<>();
Set<Selectable> changedObjects = new HashSet<>();
for (GameObject object : currentRoom.getObjects()) {
for (Selectable object : combinedObjects) {
boolean newValue = selectedObjects.contains(object);
boolean changed = object.isSelected() != newValue;
@@ -136,7 +142,7 @@ public class MouseMoveEventHandler extends AbstractEventHandler<MouseMoveEvent>
List<RerenderScreen.ScreenPart> parts = new ArrayList<>();
for (GameObject object : changedObjects) {
for (Selectable object : changedObjects) {
RoomCords cords = object.getCords();
BufferedImage objectTexture = object.getTexture();

View File

@@ -1,7 +1,7 @@
package cz.jzitnik.game;
import cz.jzitnik.game.objects.GameObject;
import cz.jzitnik.game.objects.Mob;
import cz.jzitnik.game.mobs.Mob;
import cz.jzitnik.ui.pixels.Empty;
import cz.jzitnik.ui.pixels.Pixel;
import lombok.Getter;

View File

@@ -4,16 +4,38 @@ import cz.jzitnik.game.utils.RoomCords;
import lombok.Getter;
import lombok.RequiredArgsConstructor;
import lombok.Setter;
import lombok.extern.slf4j.Slf4j;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;
@RequiredArgsConstructor
@Getter
@Slf4j
public class Player {
public enum PlayerRotation {
FRONT, BACK, LEFT, RIGHT
}
private final RoomCords playerCords;
private final ScheduledExecutorService scheduler = Executors.newScheduledThreadPool(1);
private boolean swinging = false;
@Setter
private PlayerRotation playerRotation = PlayerRotation.FRONT;
public void swing(int delayMs) {
if (swinging) {
return;
}
log.debug("Started swinging");
swinging = true;
scheduler.schedule(() -> {
swinging = false;
log.debug("Swinging done");
}, delayMs, TimeUnit.MILLISECONDS);
}
public enum PlayerRotation {
FRONT, BACK, LEFT, RIGHT
}
}

View File

@@ -0,0 +1,33 @@
package cz.jzitnik.game.mobs;
import cz.jzitnik.game.utils.RoomCords;
import cz.jzitnik.utils.DependencyManager;
import cz.jzitnik.utils.roomtasks.RoomTask;
import lombok.extern.slf4j.Slf4j;
import java.awt.image.BufferedImage;
@Slf4j
public abstract class HittableMob extends Mob {
protected int health;
public HittableMob(BufferedImage texture, RoomTask task, RoomCords cords, int initialHealth) {
super(texture, task, cords);
health = initialHealth;
}
@Override
public void interact(DependencyManager dm) {
// TODO: Swords in hand will deal more damage, for now deal always one
health--;
log.debug("Health: {}", health);
if (health <= 0) {
return;
}
// play animation
}
}

View File

@@ -0,0 +1,22 @@
package cz.jzitnik.game.mobs;
import cz.jzitnik.game.utils.Renderable;
import cz.jzitnik.game.utils.RoomCords;
import cz.jzitnik.game.utils.Selectable;
import cz.jzitnik.utils.roomtasks.RoomTask;
import lombok.Getter;
import lombok.RequiredArgsConstructor;
import lombok.Setter;
import java.awt.image.BufferedImage;
@Getter
@RequiredArgsConstructor
public abstract class Mob implements Renderable, Selectable {
private final BufferedImage texture;
private final RoomTask task;
private final RoomCords cords;
@Setter
private boolean selected = false;
}

View File

@@ -32,7 +32,7 @@ import java.util.HashSet;
import java.util.List;
@Slf4j
public final class Chest extends GameObject implements Interactable, UIClickHandler {
public final class Chest extends GameObject implements UIClickHandler {
private static final TextColor BORDER_COLOR = new TextColor.RGB(0, 0, 0);
private static final TextColor TRANSPARENT_COLOR = new TextColor.RGB(255, 255, 255);

View File

@@ -2,6 +2,7 @@ package cz.jzitnik.game.objects;
import cz.jzitnik.game.utils.Renderable;
import cz.jzitnik.game.utils.RoomCords;
import cz.jzitnik.game.utils.Selectable;
import lombok.Getter;
import lombok.RequiredArgsConstructor;
import lombok.Setter;
@@ -10,7 +11,7 @@ import java.awt.image.BufferedImage;
@Getter
@RequiredArgsConstructor
public sealed abstract class GameObject implements Renderable permits Chest {
public sealed abstract class GameObject implements Renderable, Selectable permits Chest {
private final BufferedImage texture;
private final RoomCords cords;
private final boolean selectable;

View File

@@ -1,11 +0,0 @@
package cz.jzitnik.game.objects;
import cz.jzitnik.utils.roomtasks.RoomTask;
import lombok.Getter;
import lombok.RequiredArgsConstructor;
@Getter
@RequiredArgsConstructor
public abstract class Mob {
private final RoomTask task;
}

View File

@@ -0,0 +1,12 @@
package cz.jzitnik.game.setup.mobs;
import cz.jzitnik.game.ResourceManager;
import cz.jzitnik.game.mobs.HittableMob;
import cz.jzitnik.game.utils.RoomCords;
public class Zombie extends HittableMob {
public Zombie(ResourceManager resourceManager, RoomCords cords) {
super(resourceManager.getResource(ResourceManager.Resource.CHEST), null, cords, 10);
}
}

View File

@@ -6,16 +6,11 @@ import cz.jzitnik.game.ResourceManager;
import cz.jzitnik.game.items.GameItem;
import cz.jzitnik.game.items.WoodenSword;
import cz.jzitnik.game.objects.Chest;
import cz.jzitnik.game.objects.Mob;
import cz.jzitnik.game.setup.mobs.Zombie;
import cz.jzitnik.game.utils.RoomCords;
import cz.jzitnik.states.SoundState;
import cz.jzitnik.utils.DependencyManager;
import cz.jzitnik.utils.StateManager;
import cz.jzitnik.utils.roomtasks.RoomTask;
import lombok.extern.slf4j.Slf4j;
import java.util.concurrent.TimeUnit;
@Slf4j
public class MainRoom extends GameRoom {
public MainRoom(DependencyManager dependencyManager, ResourceManager resourceManager) {
@@ -32,13 +27,7 @@ public class MainRoom extends GameRoom {
));
addObject(chest);
SoundState soundState = dependencyManager.getDependencyOrThrow(StateManager.class).getOrThrow(SoundState.class);
Mob testMob = new Mob(new RoomTask(
() -> log.debug("Sound: {}", soundState.getSoundVolume()),
1,
TimeUnit.SECONDS
)) {};
addMob(testMob);
Zombie zombie = new Zombie(resourceManager, new RoomCords(100, 100));
addMob(zombie);
}
}

View File

@@ -0,0 +1,16 @@
package cz.jzitnik.game.utils;
import cz.jzitnik.game.objects.Interactable;
import java.awt.image.BufferedImage;
public interface Selectable extends Interactable {
boolean isSelected();
void setSelected(boolean selected);
BufferedImage getTexture();
RoomCords getCords();
default boolean isSelectable() {
return true;
}
}

View File

@@ -7,12 +7,14 @@ import cz.jzitnik.events.handlers.FullRoomDrawHandler;
import cz.jzitnik.game.GameRoom;
import cz.jzitnik.game.Player;
import cz.jzitnik.game.ResourceManager;
import cz.jzitnik.game.mobs.Mob;
import cz.jzitnik.game.objects.GameObject;
import cz.jzitnik.game.utils.RoomCords;
import cz.jzitnik.states.ScreenBuffer;
import cz.jzitnik.ui.pixels.ColoredPixel;
import cz.jzitnik.ui.pixels.Empty;
import cz.jzitnik.ui.pixels.Pixel;
import lombok.extern.slf4j.Slf4j;
import java.awt.image.BufferedImage;
import java.util.HashSet;
@@ -80,11 +82,13 @@ public class RerenderUtils {
}
}
for (GameObject object : currentRoom.getObjects()) {
// TODO: Remove duplicates
for (Mob object: currentRoom.getMobs()) {
RoomCords startObjectCords = object.getCords();
BufferedImage texture = object.getTexture();
RoomCords endObjectCords = new RoomCords(startObjectCords.getX() + texture.getWidth() - 1, startObjectCords.getY() + texture.getHeight() - 1);
boolean isSelected = object.isSelected();
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()) {
int pixel = texture.getRGB(x - startObjectCords.getX(), y - startObjectCords.getY());
int alpha = (pixel >> 24) & 0xff;
@@ -94,7 +98,34 @@ public class RerenderUtils {
float factor = 1.5f; // brightness multiplier
if (alpha != 0) {
if (object.isSelected()) {
if (isSelected) {
r = Math.min(255, (int)(r * factor));
g = Math.min(255, (int)(g * factor));
b = Math.min(255, (int)(b * factor));
pixel = (alpha << 24) | (r << 16) | (g << 8) | b;
}
return new PixelResult(pixel, false);
}
}
}
for (GameObject object : currentRoom.getObjects()) {
RoomCords startObjectCords = object.getCords();
BufferedImage texture = object.getTexture();
boolean isSelected = object.isSelected();
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()) {
int pixel = texture.getRGB(x - startObjectCords.getX(), y - startObjectCords.getY());
int alpha = (pixel >> 24) & 0xff;
int r = (pixel >> 16) & 0xff;
int g = (pixel >> 8) & 0xff;
int b = pixel & 0xff;
float factor = 1.5f; // brightness multiplier
if (alpha != 0) {
if (isSelected) {
r = Math.min(255, (int)(r * factor));
g = Math.min(255, (int)(g * factor));
b = Math.min(255, (int)(b * factor));

View File

@@ -34,9 +34,9 @@ public class UIClickHandlerRepository {
return screenPart.hashCode();
}
public void handleClick(MouseAction mouseAction) {
public boolean handleClick(MouseAction mouseAction) {
if (!roomSpecificHandlers.containsKey(gameState.getCurrentRoom())) {
return;
return false;
}
Map<RerenderScreen.ScreenPart, UIClickHandler> handlers = roomSpecificHandlers.get(gameState.getCurrentRoom());
@@ -48,9 +48,11 @@ public class UIClickHandlerRepository {
if (part.isWithin(position)) {
uiClickHandler.handleClick(mouseAction);
return;
return true;
}
}
return false;
}
public void removeHandlerForCurrentRoom(int screenPartHashCode, UIClickHandler uiClickHandler) {

View File

@@ -5,7 +5,7 @@ import cz.jzitnik.annotations.PostInit;
import cz.jzitnik.annotations.injectors.InjectConfig;
import cz.jzitnik.config.ThreadPoolConfig;
import cz.jzitnik.game.GameRoom;
import cz.jzitnik.game.objects.Mob;
import cz.jzitnik.game.mobs.Mob;
import lombok.extern.slf4j.Slf4j;
import java.util.concurrent.Executors;
@@ -54,7 +54,9 @@ public class RoomTaskScheduler {
for (Mob mob : currentRoom.getMobs()) {
RoomTask task = mob.getTask();
scheduler.scheduleAtFixedRate(task.task(), 0, task.rate(), task.rateUnit());
if (task != null) {
scheduler.scheduleAtFixedRate(task.task(), 0, task.rate(), task.rateUnit());
}
}
firstRun = false;