feat: Multiplayer (#3)

Reviewed-on: https://gitea.local.jzitnik.dev/jzitnik/game/pulls/3
Co-authored-by: jzitnik-dev <email@jzitnik.dev>
Co-committed-by: jzitnik-dev <email@jzitnik.dev>
This commit is contained in:
2026-02-04 10:37:41 +00:00
parent 69fae66635
commit 5366162f43
254 changed files with 3776 additions and 1197 deletions

42
game/.gitignore vendored Normal file
View File

@@ -0,0 +1,42 @@
target/
!.mvn/wrapper/maven-wrapper.jar
!**/src/main/**/target/
!**/src/test/**/target/
.kotlin
### IntelliJ IDEA ###
.idea/modules.xml
.idea/jarRepositories.xml
.idea/compiler.xml
.idea/libraries/
.idea/FuzzierSettings.xml
*.iws
*.iml
*.ipr
### Eclipse ###
.apt_generated
.classpath
.factorypath
.project
.settings
.springBeans
.sts4-cache
### NetBeans ###
/nbproject/private/
/nbbuild/
/dist/
/nbdist/
/.nb-gradle/
build/
!**/src/main/**/build/
!**/src/test/**/build/
### VS Code ###
.vscode/
### Mac OS ###
.DS_Store
logs

3
game/.idea/.gitignore generated vendored Normal file
View File

@@ -0,0 +1,3 @@
# Default ignored files
/shelf/
/workspace.xml

13
game/.idea/encodings.xml generated Normal file
View File

@@ -0,0 +1,13 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="Encoding">
<file url="file://$PROJECT_DIR$/common/src/main/java" charset="UTF-8" />
<file url="file://$PROJECT_DIR$/common/src/main/resources" charset="UTF-8" />
<file url="file://$PROJECT_DIR$/game/src/main/java" charset="UTF-8" />
<file url="file://$PROJECT_DIR$/game/src/main/resources" charset="UTF-8" />
<file url="file://$PROJECT_DIR$/server/src/main/java" charset="UTF-8" />
<file url="file://$PROJECT_DIR$/server/src/main/resources" charset="UTF-8" />
<file url="file://$PROJECT_DIR$/src/main/java" charset="UTF-8" />
<file url="file://$PROJECT_DIR$/src/main/resources" charset="UTF-8" />
</component>
</project>

12
game/.idea/misc.xml generated Normal file
View File

@@ -0,0 +1,12 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="ExternalStorageConfigurationManager" enabled="true" />
<component name="MavenProjectsManager">
<option name="originalFiles">
<list>
<option value="$PROJECT_DIR$/pom.xml" />
</list>
</option>
</component>
<component name="ProjectRootManager" version="2" languageLevel="JDK_X" default="true" project-jdk-name="openjdk-25" project-jdk-type="JavaSDK" />
</project>

6
game/.idea/vcs.xml generated Normal file
View File

@@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="VcsDirectoryMappings">
<mapping directory="$PROJECT_DIR$" vcs="Git" />
</component>
</project>

190
game/pom.xml Normal file
View File

@@ -0,0 +1,190 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>cz.jzitnik</groupId>
<artifactId>game-parent</artifactId>
<version>1.0-SNAPSHOT</version>
</parent>
<artifactId>game</artifactId>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.11.0</version>
<configuration>
<annotationProcessorPaths>
<path>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.18.38</version>
</path>
</annotationProcessorPaths>
<source>25</source>
<target>25</target>
</configuration>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-shade-plugin</artifactId>
<version>3.2.1</version>
<executions>
<execution>
<phase>package</phase>
<goals>
<goal>shade</goal>
</goals>
</execution>
</executions>
<configuration>
<transformers>
<transformer implementation="org.apache.maven.plugins.shade.resource.ManifestResourceTransformer">
<mainClass>cz.jzitnik.client.Main</mainClass>
</transformer>
</transformers>
<shadedArtifactAttached>false</shadedArtifactAttached>
<createDependencyReducedPom>false</createDependencyReducedPom>
</configuration>
</plugin>
<plugin>
<groupId>org.codehaus.mojo</groupId>
<artifactId>exec-maven-plugin</artifactId>
<version>3.1.0</version>
<executions>
<execution>
<goals>
<goal>java</goal>
</goals>
</execution>
</executions>
<configuration>
<mainClass>cz.jzitnik.client.Main</mainClass>
<classpathScope>compile</classpathScope>
<skip>false</skip>
</configuration>
</plugin>
</plugins>
</build>
<repositories>
<repository>
<id>be.0110.repo-releases</id>
<name>0110.be repository</name>
<url>https://mvn.0110.be/releases</url>
</repository>
</repositories>
<dependencies>
<dependency>
<groupId>cz.jzitnik</groupId>
<artifactId>common</artifactId>
<version>1.0-SNAPSHOT</version>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.18.42</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>org.reflections</groupId>
<artifactId>reflections</artifactId>
<version>0.10.2</version>
</dependency>
<dependency>
<groupId>com.google.guava</groupId>
<artifactId>guava</artifactId>
<version>33.5.0-jre</version>
</dependency>
<dependency>
<groupId>tools.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
<version>3.0.4</version>
</dependency>
<dependency>
<groupId>tools.jackson.dataformat</groupId>
<artifactId>jackson-dataformat-yaml</artifactId>
<version>3.0.4</version>
</dependency>
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter</artifactId>
<version>6.0.2</version>
</dependency>
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter-api</artifactId>
<version>6.0.2</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-api</artifactId>
<version>2.0.17</version>
</dependency>
<dependency>
<groupId>ch.qos.logback</groupId>
<artifactId>logback-classic</artifactId>
<version>1.5.25</version>
</dependency>
<dependency>
<groupId>com.github.trilarion</groupId>
<artifactId>java-vorbis-support</artifactId>
<version>1.2.1</version>
</dependency>
<dependency>
<groupId>com.googlecode.lanterna</groupId>
<artifactId>lanterna</artifactId>
<version>3.1.3</version>
</dependency>
<dependency>
<groupId>org.bytedeco</groupId>
<artifactId>javacv-platform</artifactId>
<version>1.5.12</version>
</dependency>
<dependency>
<groupId>org.bytedeco</groupId>
<artifactId>ffmpeg-platform</artifactId>
<version>7.1.1-1.5.12</version>
</dependency>
<dependency>
<groupId>be.tarsos.dsp</groupId>
<artifactId>core</artifactId>
<version>2.5</version>
</dependency>
<dependency>
<groupId>be.tarsos.dsp</groupId>
<artifactId>jvm</artifactId>
<version>2.5</version>
</dependency>
<dependency>
<groupId>org.glassfish.tyrus.bundles</groupId>
<artifactId>tyrus-standalone-client</artifactId>
<version>2.2.2</version>
</dependency>
</dependencies>
</project>

View File

@@ -0,0 +1,69 @@
package cz.jzitnik.client;
import com.googlecode.lanterna.input.KeyStroke;
import com.googlecode.lanterna.screen.TerminalScreen;
import com.googlecode.lanterna.terminal.DefaultTerminalFactory;
import com.googlecode.lanterna.terminal.MouseCaptureMode;
import cz.jzitnik.client.annotations.Dependency;
import cz.jzitnik.client.annotations.injectors.InjectDependency;
import cz.jzitnik.client.annotations.injectors.InjectState;
import cz.jzitnik.client.events.KeyboardPressEvent;
import cz.jzitnik.client.events.MouseAction;
import cz.jzitnik.client.events.TerminalResizeEvent;
import cz.jzitnik.client.states.RunningState;
import cz.jzitnik.client.states.TerminalState;
import cz.jzitnik.client.utils.events.EventManager;
import lombok.extern.slf4j.Slf4j;
import java.io.IOException;
@Slf4j
@Dependency
public class Cli implements Runnable {
@InjectDependency
private EventManager eventManager;
@InjectState
private TerminalState terminalState;
@InjectState
private RunningState runningState;
@Override
public void run() {
// Start event manager thread
try (TerminalScreen terminal = new DefaultTerminalFactory()
.setMouseCaptureMode(MouseCaptureMode.CLICK_RELEASE_DRAG_MOVE)
.createScreen()) {
terminalState.setTerminalScreen(terminal);
terminalState.setTextGraphics(terminal.newTextGraphics());
terminal.setCursorPosition(null);
terminal.doResizeIfNecessary();
terminal.getTerminal().addResizeListener((ignored, terminalSize) -> {
terminal.doResizeIfNecessary();
eventManager.emitEvent(new TerminalResizeEvent(terminalSize));
});
terminal.startScreen();
eventManager.emitEvent(new TerminalResizeEvent(terminal.getTerminalSize()));
while (runningState.isRunning()) {
KeyStroke keyStroke = terminal.readInput();
if (keyStroke != null) {
if (keyStroke instanceof com.googlecode.lanterna.input.MouseAction mouse) {
eventManager.emitEvent(new MouseAction(mouse));
continue;
}
eventManager.emitEvent(new KeyboardPressEvent(keyStroke));
}
}
} catch (IOException e) {
log.error("Terminal error occurred, shutting down CLI thread.", e);
throw new RuntimeException(e);
}
}
}

View File

@@ -0,0 +1,49 @@
package cz.jzitnik.client;
import cz.jzitnik.client.annotations.injectors.InjectDependency;
import cz.jzitnik.client.game.setup.GameSetup;
import cz.jzitnik.client.socket.Client;
import cz.jzitnik.client.socket.SocketEventManager;
import cz.jzitnik.client.utils.DependencyManager;
import cz.jzitnik.client.utils.GlobalIOHandlerRepository;
import cz.jzitnik.client.utils.ScheduledTaskManager;
import cz.jzitnik.client.utils.ThreadManager;
import cz.jzitnik.client.utils.events.EventManager;
import jakarta.websocket.DeploymentException;
import org.reflections.Reflections;
import java.io.IOException;
public class Game {
private final DependencyManager dependencyManager = new DependencyManager(new Reflections("cz.jzitnik.client"));
@InjectDependency
private GameSetup gameSetup;
@InjectDependency
private EventManager eventManager;
@InjectDependency
private SocketEventManager socketEventManager;
@InjectDependency
private Cli cli;
@InjectDependency
private ThreadManager threadManager;
@InjectDependency
private ScheduledTaskManager scheduledTaskManager;
@InjectDependency
private GlobalIOHandlerRepository globalIOHandlerRepository;
public void start() throws IOException {
dependencyManager.inject(this);
eventManager.start();
socketEventManager.start();
threadManager.startAll();
scheduledTaskManager.startAll();
globalIOHandlerRepository.setup();
gameSetup.setup();
cli.run();
}
}

View File

@@ -0,0 +1,11 @@
package cz.jzitnik.client;
// events/handlers/MouseMoveEventHandler.java
import java.io.IOException;
public class Main {
public static void main(String[] args) throws IOException {
new Game().start();
}
}

View File

@@ -0,0 +1,12 @@
package cz.jzitnik.client.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 Config {
String value();
}

View File

@@ -0,0 +1,13 @@
package cz.jzitnik.client.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 Dependency {
/** Custom alias **/
Class<?> value() default Object.class;
}

View File

@@ -0,0 +1,14 @@
package cz.jzitnik.client.annotations;
import cz.jzitnik.client.utils.events.Event;
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 EventHandler {
Class<? extends Event> value();
}

View File

@@ -0,0 +1,11 @@
package cz.jzitnik.client.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.METHOD)
public @interface PostInit {
}

View File

@@ -0,0 +1,14 @@
package cz.jzitnik.client.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

@@ -0,0 +1,14 @@
package cz.jzitnik.client.annotations;
import cz.jzitnik.common.socket.SocketMessage;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
public @interface SocketEventHandler {
Class<? extends SocketMessage> value();
}

View File

@@ -0,0 +1,11 @@
package cz.jzitnik.client.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 State {
}

View File

@@ -0,0 +1,11 @@
package cz.jzitnik.client.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 ThreadRegistry {
}

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,13 @@
package cz.jzitnik.client.annotations.ui;
import com.googlecode.lanterna.input.KeyType;
import java.lang.annotation.*;
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
@Repeatable(KeyboardPressHandlers.class)
public @interface KeyboardPressHandler {
KeyType keyType() default KeyType.Character;
char character() default '\0';
}

View File

@@ -0,0 +1,12 @@
package cz.jzitnik.client.annotations.ui;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface KeyboardPressHandlers {
KeyboardPressHandler[] value();
}

View File

@@ -0,0 +1,12 @@
package cz.jzitnik.client.annotations.ui;
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.METHOD)
public @interface MouseHandler {
MouseHandlerType value();
}

View File

@@ -0,0 +1,7 @@
package cz.jzitnik.client.annotations.ui;
public enum MouseHandlerType {
CLICK,
MOVE,
ELSE,
}

View File

@@ -0,0 +1,11 @@
package cz.jzitnik.client.annotations.ui;
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.METHOD)
public @interface Render {
}

View File

@@ -0,0 +1,11 @@
package cz.jzitnik.client.annotations.ui;
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 UI {
}

View File

@@ -0,0 +1,15 @@
package cz.jzitnik.client.config;
import com.fasterxml.jackson.annotation.JsonCreator;
import com.fasterxml.jackson.annotation.JsonProperty;
import cz.jzitnik.client.annotations.Config;
@Config("core_logic.yaml")
public record CoreLogic(int itemDropDisappearMinutes) {
@JsonCreator
public CoreLogic(
@JsonProperty("itemDropDisappearMinutes") int itemDropDisappearMinutes
) {
this.itemDropDisappearMinutes = itemDropDisappearMinutes;
}
}

View File

@@ -0,0 +1,19 @@
package cz.jzitnik.client.config;
import com.fasterxml.jackson.annotation.JsonCreator;
import com.fasterxml.jackson.annotation.JsonProperty;
import cz.jzitnik.client.annotations.Config;
@Config("debugging.yaml")
public record Debugging(boolean renderColliders, boolean renderPlayerCollider, boolean showPlayerCordsLogs) {
@JsonCreator
public Debugging(
@JsonProperty("renderColliders") boolean renderColliders,
@JsonProperty("renderPlayerCollider") boolean renderPlayerCollider,
@JsonProperty("showPlayerCordsLogs") boolean showPlayerCordsLogs
) {
this.renderColliders = renderColliders;
this.renderPlayerCollider = renderPlayerCollider;
this.showPlayerCordsLogs = showPlayerCordsLogs;
}
}

View File

@@ -0,0 +1,15 @@
package cz.jzitnik.client.config;
import com.fasterxml.jackson.annotation.JsonCreator;
import com.fasterxml.jackson.annotation.JsonProperty;
import cz.jzitnik.client.annotations.Config;
@Config("microphone.yaml")
public record MicrophoneConfig(float volumeThreshold) {
@JsonCreator
public MicrophoneConfig(
@JsonProperty("volumeThreshold") float volumeThreshold
) {
this.volumeThreshold = volumeThreshold;
}
}

View File

@@ -0,0 +1,36 @@
package cz.jzitnik.client.config;
import com.fasterxml.jackson.annotation.JsonCreator;
import com.fasterxml.jackson.annotation.JsonProperty;
import cz.jzitnik.client.annotations.Config;
import cz.jzitnik.client.events.handlers.PlayerMoveEventHandler;
@Config("player.yaml")
public record PlayerConfig(
double playerReach,
int playerMoveDistance,
int playerMoveDistanceSprinting,
PlayerMoveEventHandler.SprintKey sprintKey,
int swingTimeMs,
int staminaIncreaseRateMs,
int staminaDelayMs
) {
@JsonCreator
public PlayerConfig(
@JsonProperty("playerReach") double playerReach,
@JsonProperty("playerMoveDistance") int playerMoveDistance,
@JsonProperty("playerMoveDistanceSprinting") int playerMoveDistanceSprinting,
@JsonProperty("sprintKey") PlayerMoveEventHandler.SprintKey sprintKey,
@JsonProperty("swingTimeMs") int swingTimeMs,
@JsonProperty("staminaIncreaseRateMs") int staminaIncreaseRateMs,
@JsonProperty("staminaDelayMs") int staminaDelayMs
) {
this.playerReach = playerReach;
this.playerMoveDistance = playerMoveDistance;
this.playerMoveDistanceSprinting = playerMoveDistanceSprinting;
this.sprintKey = sprintKey;
this.swingTimeMs = swingTimeMs;
this.staminaIncreaseRateMs = staminaIncreaseRateMs;
this.staminaDelayMs = staminaDelayMs;
}
}

View File

@@ -0,0 +1,17 @@
package cz.jzitnik.client.config;
import com.fasterxml.jackson.annotation.JsonCreator;
import com.fasterxml.jackson.annotation.JsonProperty;
import cz.jzitnik.client.annotations.Config;
@Config("threads.yaml")
public record ThreadPoolConfig(int eventThreadCount, int taskThreadCount) {
@JsonCreator
public ThreadPoolConfig(
@JsonProperty("eventThreadCount") int eventThreadCount,
@JsonProperty("taskThreadCount") int taskThreadCount
) {
this.eventThreadCount = eventThreadCount;
this.taskThreadCount = taskThreadCount;
}
}

View File

@@ -0,0 +1,7 @@
package cz.jzitnik.client.events;
import cz.jzitnik.client.game.objects.DroppedItem;
import cz.jzitnik.client.utils.events.Event;
public record DroppedItemRerender(DroppedItem droppedItem) implements Event {
}

View File

@@ -0,0 +1,7 @@
package cz.jzitnik.client.events;
import cz.jzitnik.client.utils.events.Event;
/** Custom event without any handler **/
public class ExitEvent implements Event {
}

View File

@@ -0,0 +1,6 @@
package cz.jzitnik.client.events;
import cz.jzitnik.client.utils.events.Event;
public class FullRedrawEvent implements Event {
}

View File

@@ -0,0 +1,15 @@
package cz.jzitnik.client.events;
import cz.jzitnik.client.utils.events.Event;
import lombok.Getter;
import lombok.NoArgsConstructor;
@NoArgsConstructor
@Getter
public class FullRoomDraw implements Event {
private boolean fullRerender = false;
public FullRoomDraw(boolean fullRerender) {
this.fullRerender = fullRerender;
}
}

View File

@@ -0,0 +1,6 @@
package cz.jzitnik.client.events;
import cz.jzitnik.client.utils.events.Event;
public class InventoryRerender implements Event {
}

View File

@@ -0,0 +1,12 @@
package cz.jzitnik.client.events;
import com.googlecode.lanterna.input.KeyStroke;
import cz.jzitnik.client.utils.events.Event;
import lombok.AllArgsConstructor;
import lombok.Getter;
@Getter
@AllArgsConstructor
public class KeyboardPressEvent implements Event {
private KeyStroke keyStroke;
}

View File

@@ -0,0 +1,15 @@
package cz.jzitnik.client.events;
import com.googlecode.lanterna.TerminalPosition;
import com.googlecode.lanterna.input.MouseActionType;
import cz.jzitnik.client.utils.events.Event;
public class MouseAction extends com.googlecode.lanterna.input.MouseAction implements Event {
public MouseAction(MouseActionType actionType, int button, TerminalPosition position) {
super(actionType, button, position);
}
public MouseAction(com.googlecode.lanterna.input.MouseAction mouseAction) {
this(mouseAction.getActionType(), mouseAction.getButton(), mouseAction.getPosition());
}
}

View File

@@ -0,0 +1,11 @@
package cz.jzitnik.client.events;
import cz.jzitnik.client.utils.events.Event;
import lombok.AllArgsConstructor;
import lombok.Getter;
@AllArgsConstructor
@Getter
public class MouseMoveEvent implements Event {
private MouseAction mouseAction;
}

View File

@@ -0,0 +1,12 @@
package cz.jzitnik.client.events;
import com.googlecode.lanterna.input.KeyStroke;
import cz.jzitnik.client.utils.events.Event;
import lombok.AllArgsConstructor;
import lombok.Getter;
@AllArgsConstructor
@Getter
public class PlayerMoveEvent implements Event {
private KeyStroke keyStroke;
}

View File

@@ -0,0 +1,6 @@
package cz.jzitnik.client.events;
import cz.jzitnik.client.utils.events.Event;
public class RenderStats implements Event {
}

View File

@@ -0,0 +1,14 @@
package cz.jzitnik.client.events;
import cz.jzitnik.client.utils.events.Event;
import lombok.AllArgsConstructor;
import lombok.Getter;
@Getter
@AllArgsConstructor
public class RerenderPart implements Event {
private int forStartX;
private int forEndX;
private int forStartY;
private int forEndY;
}

View File

@@ -0,0 +1,39 @@
package cz.jzitnik.client.events;
import com.googlecode.lanterna.TerminalPosition;
import com.googlecode.lanterna.TerminalSize;
import cz.jzitnik.client.utils.events.Event;
import lombok.AllArgsConstructor;
import lombok.Data;
public record RerenderScreen(ScreenPart[] parts) implements Event {
public RerenderScreen(ScreenPart part) {
this(new ScreenPart[]{part});
}
public static RerenderScreen full(TerminalSize terminalSize) {
return new RerenderScreen(new ScreenPart[]{ScreenPart.full(terminalSize)});
}
@Data
@AllArgsConstructor
public static class ScreenPart {
private TerminalPosition start;
private TerminalPosition end;
public static ScreenPart full(TerminalSize terminalSize) {
return new ScreenPart(
new TerminalPosition(0, 0),
new TerminalPosition(terminalSize.getColumns() - 1, terminalSize.getRows() * 2 - 1)
);
}
public boolean isWithin(TerminalPosition terminalPosition) {
return
terminalPosition.getColumn() >= start.getColumn() &&
terminalPosition.getColumn() <= end.getColumn() &&
terminalPosition.getRow() >= start.getRow() &&
terminalPosition.getRow() <= end.getRow();
}
}
}

View File

@@ -0,0 +1,7 @@
package cz.jzitnik.client.events;
import cz.jzitnik.client.events.handlers.FullRoomDrawHandler;
import cz.jzitnik.client.utils.events.Event;
public record RoomChangeEvent(FullRoomDrawHandler.DoorPosition door) implements Event {
}

View File

@@ -0,0 +1,7 @@
package cz.jzitnik.client.events;
import cz.jzitnik.client.utils.events.Event;
import cz.jzitnik.common.socket.SocketMessage;
public record SendSocketMessageEvent(SocketMessage message) implements Event {
}

View File

@@ -0,0 +1,12 @@
package cz.jzitnik.client.events;
import com.googlecode.lanterna.TerminalSize;
import cz.jzitnik.client.utils.events.Event;
import lombok.AllArgsConstructor;
import lombok.Getter;
@AllArgsConstructor
@Getter
public class TerminalResizeEvent implements Event {
private TerminalSize newSize;
}

View File

@@ -0,0 +1,6 @@
package cz.jzitnik.client.events;
import cz.jzitnik.client.utils.events.Event;
public class TerminalTooSmallEvent implements Event {
}

View File

@@ -0,0 +1,119 @@
package cz.jzitnik.client.events.handlers;
import com.googlecode.lanterna.TextColor;
import com.googlecode.lanterna.graphics.TextGraphics;
import cz.jzitnik.client.annotations.EventHandler;
import cz.jzitnik.client.annotations.injectors.InjectState;
import cz.jzitnik.client.events.RerenderScreen;
import cz.jzitnik.client.game.Constants;
import cz.jzitnik.client.states.RenderState;
import cz.jzitnik.client.states.ScreenBuffer;
import cz.jzitnik.client.states.TerminalState;
import cz.jzitnik.client.ui.pixels.AlphaPixel;
import cz.jzitnik.client.ui.pixels.ColoredPixel;
import cz.jzitnik.client.ui.pixels.Empty;
import cz.jzitnik.client.ui.pixels.Pixel;
import cz.jzitnik.client.utils.events.AbstractEventHandler;
import lombok.extern.slf4j.Slf4j;
import java.io.IOException;
@Slf4j
@EventHandler(RerenderScreen.class)
public class CliHandler extends AbstractEventHandler<RerenderScreen> {
@InjectState
private TerminalState terminalState;
@InjectState
private ScreenBuffer screenBuffer;
@InjectState
private RenderState renderState;
@Override
public void handle(RerenderScreen event) {
if (renderState.isTerminalTooSmall()) {
return;
}
var parts = event.parts();
var buffer = screenBuffer.getRenderedBuffer();
var globalOverrideBuffer = screenBuffer.getGlobalOverrideBuffer();
var terminalScreen = terminalState.getTerminalScreen();
var tg = terminalState.getTextGraphics();
for (RerenderScreen.ScreenPart part : parts) {
var start = part.getStart();
int startYNormalized = (start.getRow() / 2) * 2; // Round to multiple of 2 down
var end = part.getEnd();
int endYNormalized = ((end.getRow() - 1) / 2) * 2; // Round to multiple of 2 ceil
for (int y = startYNormalized; y <= endYNormalized; y += 2) {
for (int x = start.getColumn(); x <= end.getColumn(); x++) {
try {
Pixel topPixel = getPixel(buffer[y][x], globalOverrideBuffer[y][x]);
Pixel bottomPixel = (y + 1 <= end.getRow())
? getPixel(buffer[y + 1][x], globalOverrideBuffer[y + 1][x])
: new Empty();
TextColor topColor = topPixel instanceof Empty
? Constants.BACKGROUND_COLOR
: topPixel.getColor();
TextColor bottomColor = bottomPixel instanceof Empty
? Constants.BACKGROUND_COLOR
: bottomPixel.getColor();
drawHalfPixel(tg, x, y / 2, topColor, bottomColor);
} catch (ArrayIndexOutOfBoundsException ignored) {
// Random error, ignore
}
}
}
}
try {
terminalScreen.refresh();
} catch (IOException e) {
throw new RuntimeException(e);
}
}
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) {
tg.setBackgroundColor(topColor); // upper half
tg.setForegroundColor(bottomColor); // lower half
tg.setCharacter(x, y, '▄');
}
}

View File

@@ -0,0 +1,200 @@
package cz.jzitnik.client.events.handlers;
import com.googlecode.lanterna.TerminalPosition;
import com.googlecode.lanterna.TerminalSize;
import com.googlecode.lanterna.TextColor;
import cz.jzitnik.client.annotations.EventHandler;
import cz.jzitnik.client.annotations.injectors.InjectDependency;
import cz.jzitnik.client.annotations.injectors.InjectState;
import cz.jzitnik.client.events.RerenderScreen;
import cz.jzitnik.client.game.dialog.Dialog;
import cz.jzitnik.client.game.dialog.OnEnd;
import cz.jzitnik.client.states.DialogState;
import cz.jzitnik.client.states.ScreenBuffer;
import cz.jzitnik.client.states.TerminalState;
import cz.jzitnik.client.ui.pixels.AlphaPixel;
import cz.jzitnik.client.ui.pixels.ColoredPixel;
import cz.jzitnik.client.ui.pixels.Empty;
import cz.jzitnik.client.utils.DependencyManager;
import cz.jzitnik.client.utils.TextRenderer;
import cz.jzitnik.client.utils.events.AbstractEventHandler;
import cz.jzitnik.client.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> {
@InjectState
private DialogState dialogState;
@InjectState
private TerminalState terminalState;
@InjectState
private ScreenBuffer screenBuffer;
@InjectDependency
private EventManager eventManager;
@InjectDependency
private TextRenderer textRenderer;
@InjectDependency
private DependencyManager dependencyManager;
private static final int WIDTH = 350;
private static final int MARGIN_BOTTOM = 15;
public static final int PADDING = 7;
private static final int BUTTON_TEXT_PADDING = 4;
private static final int QUESTION_ACTIONS_GAP = 10;
public static final int BUTTON_HEIGHT = 15;
public static final int BUTTON_PADDING = 5;
private static final float FONT_SIZE = 15f;
public static int calculateButtonHeight(Dialog dialog) {
if (dialog.getOnEnd() instanceof OnEnd.AskQuestion(OnEnd.AskQuestion.Answer[] answers)) {
return answers.length * BUTTON_HEIGHT + (answers.length - 1) * BUTTON_PADDING;
} else {
return 0;
}
}
public static int getYStartButtons(TextRenderer textRenderer, Dialog dialog) {
var textSize = textRenderer.measureText(dialog.getText(), WIDTH - PADDING * 2, FONT_SIZE);
return PADDING + textSize.height + BUTTON_PADDING;
}
public static TerminalSize getSize(TextRenderer textRenderer, Dialog dialog) {
var textSize = textRenderer.measureText(dialog.getText(), WIDTH - PADDING * 2, FONT_SIZE);
return new TerminalSize(300, PADDING + textSize.height + (
dialog.getOnEnd() instanceof OnEnd.AskQuestion ? BUTTON_PADDING + calculateButtonHeight(dialog) : 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) {
boolean onlyLast = dialogState.getCurrentDialog() == 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, false));
}
}
dialogState.setRenderInProgress(true);
try {
for (int i = onlyLast ? animation.length : 0; i <= animation.length; i++) {
var buf = animation[Math.min(i, animation.length - 1)];
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 (i == animation.length && 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), dialogState.getHoveredButtonIndex() == buttonIndex ? 0.8f : 0.6f);
} 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());
}
dialogState.setRenderInProgress(false);
next(onEnd, start, size);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
private void next(OnEnd onEnd, TerminalPosition start, TerminalSize size) throws InterruptedException {
if (onEnd instanceof OnEnd.Continue(Dialog nextDialog)) {
Thread.sleep(1000);
for (int y = start.getRow(); y < start.getRow() + size.getRows(); y++) {
for (int x = start.getColumn(); x < start.getColumn() + size.getColumns(); x++) {
screenBuffer.getGlobalOverrideBuffer()[y][x] = new Empty();
}
}
if (nextDialog == null) {
dialogState.setCurrentDialog(null);
eventManager.emitEvent(
new RerenderScreen(
new RerenderScreen.ScreenPart(
start,
new TerminalPosition(start.getColumn() + size.getColumns(), start.getRow() + size.getRows())
)
)
);
} else {
eventManager.emitEvent(nextDialog);
}
} else if (onEnd instanceof OnEnd.RunCode(Runnable runnable, OnEnd end)) {
dependencyManager.inject(runnable);
runnable.run();
next(end, start, size);
}
}
}

View File

@@ -0,0 +1,30 @@
package cz.jzitnik.client.events.handlers;
import cz.jzitnik.client.annotations.EventHandler;
import cz.jzitnik.client.annotations.injectors.InjectDependency;
import cz.jzitnik.client.events.DroppedItemRerender;
import cz.jzitnik.client.events.RerenderPart;
import cz.jzitnik.common.models.coordinates.RoomCords;
import cz.jzitnik.client.utils.events.AbstractEventHandler;
import cz.jzitnik.client.utils.events.EventManager;
import java.awt.image.BufferedImage;
@EventHandler(DroppedItemRerender.class)
public class DroppedItemRerenderHandler extends AbstractEventHandler<DroppedItemRerender> {
@InjectDependency
private EventManager eventManager;
@Override
public void handle(DroppedItemRerender event) {
RoomCords droppedItemCords = event.droppedItem().getCords();
BufferedImage droppedItemTexture = event.droppedItem().getTexture();
eventManager.emitEvent(new RerenderPart(
droppedItemCords.getX(),
droppedItemCords.getX() + droppedItemTexture.getWidth(),
droppedItemCords.getY(),
droppedItemCords.getY() + droppedItemTexture.getHeight()
));
}
}

View File

@@ -0,0 +1,35 @@
package cz.jzitnik.client.events.handlers;
import cz.jzitnik.client.annotations.EventHandler;
import cz.jzitnik.client.annotations.injectors.InjectDependency;
import cz.jzitnik.client.annotations.injectors.InjectState;
import cz.jzitnik.client.events.ExitEvent;
import cz.jzitnik.client.states.RunningState;
import cz.jzitnik.client.utils.ScheduledTaskManager;
import cz.jzitnik.client.utils.ThreadManager;
import cz.jzitnik.client.utils.events.AbstractEventHandler;
import cz.jzitnik.client.utils.roomtasks.RoomTaskScheduler;
@EventHandler(ExitEvent.class)
public class ExitEventHandler extends AbstractEventHandler<ExitEvent> {
@InjectDependency
private ThreadManager threadManager;
@InjectState
private RunningState runningState;
@InjectDependency
private RoomTaskScheduler roomTaskScheduler;
@InjectDependency
private ScheduledTaskManager scheduledTaskManager;
@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

@@ -0,0 +1,32 @@
package cz.jzitnik.client.events.handlers;
import com.googlecode.lanterna.screen.Screen;
import cz.jzitnik.client.annotations.EventHandler;
import cz.jzitnik.client.annotations.injectors.InjectDependency;
import cz.jzitnik.client.annotations.injectors.InjectState;
import cz.jzitnik.client.events.FullRedrawEvent;
import cz.jzitnik.client.events.FullRoomDraw;
import cz.jzitnik.client.states.TerminalState;
import cz.jzitnik.client.utils.events.AbstractEventHandler;
import cz.jzitnik.client.utils.events.EventManager;
import java.io.IOException;
@EventHandler(FullRedrawEvent.class)
public class FullRedrawEventHandler extends AbstractEventHandler<FullRedrawEvent> {
@InjectDependency
private EventManager eventManager;
@InjectState
private TerminalState terminalState;
@Override
public void handle(FullRedrawEvent event) {
terminalState.getTerminalScreen().clear();
try {
terminalState.getTerminalScreen().refresh(Screen.RefreshType.COMPLETE);
} catch (IOException _) {
}
eventManager.emitEvent(new FullRoomDraw(true));
}
}

View File

@@ -0,0 +1,112 @@
package cz.jzitnik.client.events.handlers;
import com.googlecode.lanterna.TerminalPosition;
import com.googlecode.lanterna.TerminalSize;
import com.googlecode.lanterna.screen.TerminalScreen;
import cz.jzitnik.client.annotations.EventHandler;
import cz.jzitnik.client.annotations.injectors.InjectConfig;
import cz.jzitnik.client.annotations.injectors.InjectDependency;
import cz.jzitnik.client.annotations.injectors.InjectState;
import cz.jzitnik.client.config.Debugging;
import cz.jzitnik.client.events.FullRoomDraw;
import cz.jzitnik.client.events.RerenderScreen;
import cz.jzitnik.client.events.TerminalTooSmallEvent;
import cz.jzitnik.client.game.GameRoom;
import cz.jzitnik.client.game.GameState;
import cz.jzitnik.client.game.Player;
import cz.jzitnik.client.game.ResourceManager;
import cz.jzitnik.client.states.RenderState;
import cz.jzitnik.client.states.ScreenBuffer;
import cz.jzitnik.client.states.TerminalState;
import cz.jzitnik.client.utils.GlobalIOHandlerRepository;
import cz.jzitnik.client.utils.RerenderUtils;
import cz.jzitnik.client.utils.events.AbstractEventHandler;
import cz.jzitnik.client.utils.events.EventManager;
import cz.jzitnik.client.utils.roomtasks.RoomTaskScheduler;
import lombok.extern.slf4j.Slf4j;
import java.awt.image.BufferedImage;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;
@Slf4j
@EventHandler(FullRoomDraw.class)
public class FullRoomDrawHandler extends AbstractEventHandler<FullRoomDraw> {
private final ScheduledExecutorService scheduler = Executors.newScheduledThreadPool(1);
@InjectState
private GameState gameState;
@InjectState
private ScreenBuffer screenBuffer;
@InjectDependency
private ResourceManager resourceManager;
@InjectState
private TerminalState terminalState;
@InjectDependency
private EventManager eventManager;
@InjectState
private RenderState renderState;
@InjectConfig
private Debugging debugging;
@InjectDependency
private RoomTaskScheduler roomTaskScheduler;
@InjectDependency
private GlobalIOHandlerRepository globalIOHandlerRepository;
@Override
public void handle(FullRoomDraw event) {
try {
log.debug("Rendering full room");
TerminalScreen terminalScreen = terminalState.getTerminalScreen();
List<RerenderScreen.ScreenPart> partsToRerender = new ArrayList<>();
GameRoom currentRoom = gameState.getCurrentRoom();
BufferedImage room = resourceManager.getResource(currentRoom.getTexture());
Player player = gameState.getPlayer();
BufferedImage playerTexture = player.getTexture(resourceManager);
TerminalSize terminalSize = terminalScreen.getTerminalSize();
int width = room.getWidth();
int height = room.getHeight();
var start = RerenderUtils.getStart(room, terminalSize);
int startX = start.getX();
int startY = start.getY();
RerenderUtils.rerenderPart(0, width - 1, 0, height - 1, startX, startY, currentRoom, room, player, screenBuffer, resourceManager, debugging, gameState.getOtherPlayers());
if (event.isFullRerender()) {
globalIOHandlerRepository.renderAll();
}
partsToRerender.add(new RerenderScreen.ScreenPart(
new TerminalPosition(startX, startY),
new TerminalPosition(startX + width, startY + height - 1)
));
if (renderState.isFirstRender() || event.isFullRerender()) {
eventManager.emitEvent(RerenderScreen.full(terminalSize));
renderState.setFirstRender(false);
scheduler.schedule(() -> roomTaskScheduler.setupNewSchedulers(currentRoom), 200, TimeUnit.MILLISECONDS);
} else {
eventManager.emitEvent(new RerenderScreen(partsToRerender.toArray(RerenderScreen.ScreenPart[]::new)));
}
renderState.setTerminalTooSmall(false);
} catch (ArrayIndexOutOfBoundsException e) {
// Screen too small to fit the room
eventManager.emitEvent(new TerminalTooSmallEvent());
renderState.setTerminalTooSmall(true);
roomTaskScheduler.shutdownTasks();
//log.error("Terminal too small", e);
}
}
public enum DoorPosition {
TOP,
LEFT,
RIGHT,
BOTTOM,
}
}

View File

@@ -0,0 +1,28 @@
package cz.jzitnik.client.events.handlers;
import com.googlecode.lanterna.TerminalPosition;
import cz.jzitnik.client.annotations.EventHandler;
import cz.jzitnik.client.annotations.injectors.InjectDependency;
import cz.jzitnik.client.events.InventoryRerender;
import cz.jzitnik.client.events.RerenderScreen;
import cz.jzitnik.client.ui.Inventory;
import cz.jzitnik.client.utils.events.AbstractEventHandler;
import cz.jzitnik.client.utils.events.EventManager;
@EventHandler(InventoryRerender.class)
public class InventoryRerenderHandler extends AbstractEventHandler<InventoryRerender> {
@InjectDependency
private EventManager eventManager;
@InjectDependency
private Inventory inventory;
@Override
public void handle(InventoryRerender event) {
inventory.renderInventoryRerender();
eventManager.emitEvent(new RerenderScreen(new RerenderScreen.ScreenPart(
new TerminalPosition(inventory.getOffsetX(), inventory.getOffsetY()),
new TerminalPosition(inventory.getOffsetX() + Inventory.INVENTORY_WIDTH, inventory.getOffsetY() + Inventory.INVENTORY_HEIGHT)
)));
}
}

View File

@@ -0,0 +1,28 @@
package cz.jzitnik.client.events.handlers;
import cz.jzitnik.client.annotations.EventHandler;
import cz.jzitnik.client.annotations.injectors.InjectDependency;
import cz.jzitnik.client.annotations.injectors.InjectState;
import cz.jzitnik.client.events.KeyboardPressEvent;
import cz.jzitnik.client.game.GameState;
import cz.jzitnik.client.utils.GlobalIOHandlerRepository;
import cz.jzitnik.client.utils.events.AbstractEventHandler;
@EventHandler(KeyboardPressEvent.class)
public class KeyboardPressEventHandler extends AbstractEventHandler<KeyboardPressEvent> {
@InjectState
private GameState gameState;
@InjectDependency
private GlobalIOHandlerRepository globalIOHandlerRepository;
@Override
public void handle(KeyboardPressEvent event) {
if (gameState.getScreen() != null) {
gameState.getScreen().handleKeyboardAction(event);
return;
}
globalIOHandlerRepository.keyboard(event);
}
}

View File

@@ -0,0 +1,86 @@
package cz.jzitnik.client.events.handlers;
import cz.jzitnik.client.annotations.EventHandler;
import cz.jzitnik.client.annotations.injectors.InjectConfig;
import cz.jzitnik.client.annotations.injectors.InjectDependency;
import cz.jzitnik.client.annotations.injectors.InjectState;
import cz.jzitnik.client.config.PlayerConfig;
import cz.jzitnik.client.events.MouseAction;
import cz.jzitnik.client.events.MouseMoveEvent;
import cz.jzitnik.client.game.GameState;
import cz.jzitnik.client.game.utils.Selectable;
import cz.jzitnik.client.states.RenderState;
import cz.jzitnik.client.utils.DependencyManager;
import cz.jzitnik.client.utils.UIRoomClickHandlerRepository;
import cz.jzitnik.client.utils.events.AbstractEventHandler;
import cz.jzitnik.client.utils.events.EventManager;
import java.util.Optional;
import java.util.stream.Stream;
@EventHandler(MouseAction.class)
public class MouseActionEventHandler extends AbstractEventHandler<MouseAction> {
@InjectDependency
private EventManager eventManager;
@InjectDependency
private UIRoomClickHandlerRepository uiRoomClickHandlerRepository;
@InjectState
private GameState gameState;
@InjectState
private RenderState renderState;
@InjectConfig
private PlayerConfig playerConfig;
@InjectDependency
private DependencyManager dependencyManager;
@Override
public void handle(MouseAction event) {
if (gameState.getScreen() != null) {
gameState.getScreen().handleMouseAction(event);
return;
}
if (renderState.isTerminalTooSmall()) {
return;
}
switch (event.getActionType()) {
case MOVE -> {
boolean registered = uiRoomClickHandlerRepository.handleMove(event);
if (!registered) {
eventManager.emitEvent(new MouseMoveEvent(event));
}
}
case CLICK_RELEASE -> {
boolean clicked = uiRoomClickHandlerRepository.handleClick(event);
if (clicked || gameState.getPlayer().isSwinging()) {
return;
}
Stream<? extends Selectable> combined = Stream.concat(
Stream.concat(
gameState.getCurrentRoom().getMobs().stream(),
gameState.getCurrentRoom().getObjects().stream()),
gameState.getCurrentRoom().getDroppedItems().stream()
);
Optional<? extends Selectable> object = combined.filter(Selectable::isSelected).findFirst();
gameState.getPlayer().swing(playerConfig.swingTimeMs());
object.ifPresent(selectable -> {
dependencyManager.inject(selectable);
selectable.interact();
});
}
default -> uiRoomClickHandlerRepository.handleElse(event);
}
}
}

View File

@@ -0,0 +1,196 @@
package cz.jzitnik.client.events.handlers;
import com.googlecode.lanterna.TerminalPosition;
import com.googlecode.lanterna.input.MouseActionType;
import cz.jzitnik.client.annotations.EventHandler;
import cz.jzitnik.client.annotations.injectors.InjectConfig;
import cz.jzitnik.client.annotations.injectors.InjectDependency;
import cz.jzitnik.client.annotations.injectors.InjectState;
import cz.jzitnik.client.config.Debugging;
import cz.jzitnik.client.config.PlayerConfig;
import cz.jzitnik.client.events.MouseAction;
import cz.jzitnik.client.events.MouseMoveEvent;
import cz.jzitnik.client.events.RerenderScreen;
import cz.jzitnik.client.game.GameRoom;
import cz.jzitnik.client.game.GameState;
import cz.jzitnik.client.game.Player;
import cz.jzitnik.client.game.ResourceManager;
import cz.jzitnik.common.models.coordinates.RoomCords;
import cz.jzitnik.client.game.utils.Selectable;
import cz.jzitnik.client.states.ScreenBuffer;
import cz.jzitnik.client.states.TerminalState;
import cz.jzitnik.client.utils.RerenderUtils;
import cz.jzitnik.client.utils.events.AbstractEventHandler;
import cz.jzitnik.client.utils.events.EventManager;
import lombok.extern.slf4j.Slf4j;
import java.awt.image.BufferedImage;
import java.util.ArrayList;
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)
public class MouseMoveEventHandler extends AbstractEventHandler<MouseMoveEvent> {
private MouseMoveEvent lastEvent = new MouseMoveEvent(new MouseAction(MouseActionType.MOVE, 1, new TerminalPosition(0, 0)));
@InjectState
private GameState gameState;
@InjectState
private ScreenBuffer screenBuffer;
@InjectState
private TerminalState terminalState;
@InjectDependency
private ResourceManager resourceManager;
@InjectDependency
private EventManager eventManager;
@InjectConfig
private Debugging debugging;
@InjectConfig
private PlayerConfig playerConfig;
private double distancePointToRect(
double px, double py,
double rectTopLeftX, double rectTopLeftY,
double rectBottomRightX, double rectBottomRightY) {
double minX = Math.min(rectTopLeftX, rectBottomRightX);
double maxX = Math.max(rectTopLeftX, rectBottomRightX);
double minY = Math.min(rectTopLeftY, rectBottomRightY);
double maxY = Math.max(rectTopLeftY, rectBottomRightY);
boolean isInside = (px > minX && px < maxX && py > minY && py < maxY);
if (isInside) {
return 0;
} else {
double closestX = Math.max(minX, Math.min(px, maxX));
double closestY = Math.max(minY, Math.min(py, maxY));
double dx = px - closestX;
double dy = py - closestY;
return Math.sqrt(dx * dx + dy * dy);
}
}
@Override
public void handle(MouseMoveEvent event) {
if (event.getMouseAction() != null) {
this.lastEvent = event;
}
MouseAction mouseAction = lastEvent.getMouseAction();
int mouseX = mouseAction.getPosition().getColumn();
int mouseY = mouseAction.getPosition().getRow();
GameRoom currentRoom = gameState.getCurrentRoom();
BufferedImage room = resourceManager.getResource(currentRoom.getTexture());
Player player = gameState.getPlayer();
RoomCords playerCords = player.getPlayerCords();
BufferedImage playerTexture = player.getTexture(resourceManager);
var start = RerenderUtils.getStart(room, terminalState.getTerminalScreen().getTerminalSize());
int startX = start.getX();
int startY = start.getY();
/*List<? extends Selectable> _combinedObjects = Stream.of(
currentRoom.getObjects().stream(),
currentRoom.getMobs().stream(),
currentRoom.getDroppedItems().stream())
.flatMap(Function.identity()).toList();*/ // For some reason doesn't compile
List<? extends Selectable> combinedObjects = Stream.concat(Stream.concat(
currentRoom.getObjects().stream(),
currentRoom.getMobs().stream()
), currentRoom.getDroppedItems().stream()).toList();
Set<Selectable> selectedObjects = combinedObjects.stream().filter(gameObject -> {
if (!gameObject.isSelectable()) return false;
BufferedImage texture = gameObject.getTexture();
RoomCords cords = gameObject.getCords();
int relativeMouseX = mouseX - startX;
int relativeMouseY = (mouseY * 2 - startY);
int objXStart = cords.getX();
int objYStart = cords.getY();
int objXEnd = cords.getX() + texture.getWidth();
int objYEnd = cords.getY() + texture.getHeight();
int playerMiddleX = playerCords.getX() + (playerTexture.getWidth() / 2);
int playerMiddleY = playerCords.getY() + (playerTexture.getHeight() / 2);
double distance = distancePointToRect(
playerMiddleX, playerMiddleY,
objXStart, objYStart,
objXEnd, objYEnd
);
if (distance > playerConfig.playerReach()) {
return false;
}
return
relativeMouseX >= cords.getX() &&
relativeMouseX < cords.getX() + texture.getWidth() &&
relativeMouseY >= cords.getY() &&
relativeMouseY < cords.getY() + texture.getHeight();
}).collect(Collectors.toSet());
Set<Selectable> changedObjects = new HashSet<>();
for (Selectable object : combinedObjects) {
boolean newValue = selectedObjects.contains(object);
boolean changed = object.isSelected() != newValue;
if (changed) {
object.setSelected(newValue);
changedObjects.add(object);
}
}
List<RerenderScreen.ScreenPart> parts = new ArrayList<>();
for (Selectable object : changedObjects) {
RoomCords cords = object.getCords();
BufferedImage objectTexture = object.getTexture();
int forStartX = cords.getX();
int forStartY = cords.getY();
int forEndX = cords.getX() + objectTexture.getWidth() - 1;
int forEndY = cords.getY() + objectTexture.getHeight();
RerenderUtils.rerenderPart(
forStartX,
forEndX,
forStartY,
forEndY,
startX,
startY,
currentRoom,
room,
player,
screenBuffer,
resourceManager,
debugging,
gameState.getOtherPlayers()
);
parts.add(new RerenderScreen.ScreenPart(
new TerminalPosition(forStartX, forStartY),
new TerminalPosition(forEndX * 2 + 1 + startX, forEndY + startY)
));
}
if (!parts.isEmpty()) {
eventManager.emitEvent(new RerenderScreen(parts.toArray(RerenderScreen.ScreenPart[]::new)));
}
}
}

View File

@@ -0,0 +1,167 @@
package cz.jzitnik.client.events.handlers;
import com.googlecode.lanterna.TerminalPosition;
import com.googlecode.lanterna.input.KeyStroke;
import cz.jzitnik.client.annotations.EventHandler;
import cz.jzitnik.client.annotations.injectors.InjectConfig;
import cz.jzitnik.client.annotations.injectors.InjectDependency;
import cz.jzitnik.client.annotations.injectors.InjectState;
import cz.jzitnik.client.config.Debugging;
import cz.jzitnik.client.config.PlayerConfig;
import cz.jzitnik.client.events.*;
import cz.jzitnik.client.game.*;
import cz.jzitnik.common.models.coordinates.RoomCords;
import cz.jzitnik.client.states.RenderState;
import cz.jzitnik.client.states.ScreenBuffer;
import cz.jzitnik.client.states.PlayerMovementState;
import cz.jzitnik.client.states.TerminalState;
import cz.jzitnik.client.ui.Stats;
import cz.jzitnik.client.utils.RerenderUtils;
import cz.jzitnik.client.utils.events.AbstractEventHandler;
import cz.jzitnik.client.utils.events.Event;
import cz.jzitnik.client.utils.events.EventManager;
import cz.jzitnik.common.socket.messages.player.PlayerMove;
import cz.jzitnik.common.socket.messages.player.PlayerRotation;
import lombok.extern.slf4j.Slf4j;
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;
@InjectState
private PlayerMovementState playerMovementState;
@InjectDependency
private Stats stats;
@Override
public void handle(PlayerMoveEvent event) {
if (renderState.isTerminalTooSmall()) {
return;
}
KeyStroke keyStroke = event.getKeyStroke();
Player player = gameState.getPlayer();
RoomCords playerCords = player.getPlayerCords();
GameRoom currentRoom = gameState.getCurrentRoom();
boolean isSprinting = player.getStamina() > 0 && switch (playerConfig.sprintKey()) {
case CTRL -> event.getKeyStroke().isCtrlDown();
case SHIFT -> event.getKeyStroke().isShiftDown();
case ALT -> event.getKeyStroke().isAltDown();
};
int moveStep = isSprinting ? playerConfig.playerMoveDistanceSprinting() : playerConfig.playerMoveDistance();
int originalPlayerX = playerCords.getX();
int originalPlayerY = playerCords.getY();
switch (keyStroke.getCharacter()) {
case 'w' -> {
if (originalPlayerY <= 10) {
if (originalPlayerX >= 80 && originalPlayerX <= 105) {
player.setPlayerRotation(PlayerRotation.BACK);
eventManager.emitEvent(new RoomChangeEvent(FullRoomDrawHandler.DoorPosition.TOP));
}
return;
}
playerCords.updateCordsWithColliders(currentRoom.getColliders(), player.getPlayerCords().getX(), playerCords.getY() - moveStep, player.getCollider());
player.setPlayerRotation(PlayerRotation.BACK);
}
case 'a' -> {
if (originalPlayerX <= 30) {
if (originalPlayerY >= 35 && originalPlayerY <= 65) {
player.setPlayerRotation(PlayerRotation.LEFT);
eventManager.emitEvent(new RoomChangeEvent(FullRoomDrawHandler.DoorPosition.LEFT));
}
return;
}
playerCords.updateCordsWithColliders(currentRoom.getColliders(), player.getPlayerCords().getX() - moveStep, playerCords.getY(), player.getCollider());
player.setPlayerRotation(PlayerRotation.LEFT);
}
case 's' -> {
if (originalPlayerY >= 110) {
if (originalPlayerX >= 75 && originalPlayerX <= 105) {
player.setPlayerRotation(PlayerRotation.FRONT);
eventManager.emitEvent(new RoomChangeEvent(FullRoomDrawHandler.DoorPosition.BOTTOM));
}
return;
}
playerCords.updateCordsWithColliders(currentRoom.getColliders(), player.getPlayerCords().getX(), playerCords.getY() + moveStep, player.getCollider());
player.setPlayerRotation(PlayerRotation.FRONT);
}
case 'd' -> {
if (originalPlayerX >= 155) {
if (originalPlayerY >= 40 && originalPlayerY <= 60) {
player.setPlayerRotation(PlayerRotation.RIGHT);
eventManager.emitEvent(new RoomChangeEvent(FullRoomDrawHandler.DoorPosition.RIGHT));
}
return;
}
playerCords.updateCordsWithColliders(currentRoom.getColliders(), player.getPlayerCords().getX() + moveStep, playerCords.getY(), player.getCollider());
player.setPlayerRotation(PlayerRotation.RIGHT);
}
}
playerMovementState.setLastMovement(System.currentTimeMillis());
if (isSprinting) {
player.decreaseStamina();
stats.rerender();
}
int newPlayerX = playerCords.getX();
int newPlayerY = playerCords.getY();
if (debugging.showPlayerCordsLogs()) {
log.debug("x: {}, y: {}", newPlayerX, newPlayerY);
}
BufferedImage playerTexture = player.getTexture(resourceManager);
int forStartX = Math.min(originalPlayerX, newPlayerX);
int forStartY = Math.min(originalPlayerY, newPlayerY);
int forEndX = Math.max(originalPlayerX, newPlayerX) + playerTexture.getWidth();
int forEndY = Math.max(originalPlayerY, newPlayerY) + playerTexture.getHeight();
BufferedImage room = resourceManager.getResource(currentRoom.getTexture());
var start = RerenderUtils.getStart(room, terminalState.getTerminalScreen().getTerminalSize());
int startX = start.getX();
int startY = start.getY();
RerenderUtils.rerenderPart(forStartX, forEndX, forStartY, forEndY, startX, startY, currentRoom, room, player, screenBuffer, resourceManager, debugging, gameState.getOtherPlayers());
eventManager.emitEvent(new Event[]{
new SendSocketMessageEvent(new PlayerMove(playerCords, player.getPlayerRotation())),
new MouseMoveEvent(null),
new RerenderScreen(
new RerenderScreen.ScreenPart[]{
new RerenderScreen.ScreenPart(
new TerminalPosition(forStartX + startX, forStartY + startY),
new TerminalPosition(forEndX + 1 + startX, forEndY + startY)
),
new RerenderScreen.ScreenPart(
new TerminalPosition(Stats.OFFSET_X, Stats.OFFSET_X),
new TerminalPosition(Stats.OFFSET_X + Stats.WIDTH, Stats.OFFSET_Y + Stats.HEIGHT)
)
}
)
});
}
public static enum SprintKey {
CTRL,
SHIFT,
ALT
}
}

View File

@@ -0,0 +1,28 @@
package cz.jzitnik.client.events.handlers;
import com.googlecode.lanterna.TerminalPosition;
import cz.jzitnik.client.annotations.EventHandler;
import cz.jzitnik.client.annotations.injectors.InjectDependency;
import cz.jzitnik.client.events.RenderStats;
import cz.jzitnik.client.events.RerenderScreen;
import cz.jzitnik.client.ui.Stats;
import cz.jzitnik.client.utils.events.AbstractEventHandler;
import cz.jzitnik.client.utils.events.EventManager;
@EventHandler(RenderStats.class)
public class RenderStatsHandler extends AbstractEventHandler<RenderStats> {
@InjectDependency
private EventManager eventManager;
@InjectDependency
private Stats stats;
@Override
public void handle(RenderStats event) {
stats.rerender();
eventManager.emitEvent(new RerenderScreen(new RerenderScreen.ScreenPart(
new TerminalPosition(Stats.OFFSET_X, Stats.OFFSET_X),
new TerminalPosition(Stats.OFFSET_X + Stats.WIDTH, Stats.OFFSET_Y + Stats.HEIGHT)
)));
}
}

View File

@@ -0,0 +1,78 @@
package cz.jzitnik.client.events.handlers;
import com.googlecode.lanterna.TerminalPosition;
import cz.jzitnik.client.annotations.EventHandler;
import cz.jzitnik.client.annotations.injectors.InjectConfig;
import cz.jzitnik.client.annotations.injectors.InjectDependency;
import cz.jzitnik.client.annotations.injectors.InjectState;
import cz.jzitnik.client.config.Debugging;
import cz.jzitnik.client.events.RerenderPart;
import cz.jzitnik.client.events.RerenderScreen;
import cz.jzitnik.client.game.GameRoom;
import cz.jzitnik.client.game.GameState;
import cz.jzitnik.client.game.ResourceManager;
import cz.jzitnik.common.models.coordinates.RoomCords;
import cz.jzitnik.client.states.ScreenBuffer;
import cz.jzitnik.client.states.TerminalState;
import cz.jzitnik.client.utils.RerenderUtils;
import cz.jzitnik.client.utils.events.AbstractEventHandler;
import cz.jzitnik.client.utils.events.EventManager;
import java.awt.image.BufferedImage;
@EventHandler(RerenderPart.class)
public class RerenderPartHandler extends AbstractEventHandler<RerenderPart> {
@InjectState
private TerminalState terminalState;
@InjectState
private GameState gameState;
@InjectState
private ScreenBuffer screenBuffer;
@InjectConfig
private Debugging debugging;
@InjectDependency
private ResourceManager resourceManager;
@InjectDependency
private EventManager eventManager;
@Override
public void handle(RerenderPart event) {
int forStartX = event.getForStartX();
int forEndX = event.getForEndX();
int forStartY = event.getForStartY();
int forEndY = event.getForEndY();
GameRoom currentRoom = gameState.getCurrentRoom();
BufferedImage room = resourceManager.getResource(currentRoom.getTexture());
RoomCords start = RerenderUtils.getStart(room, terminalState.getTerminalScreen().getTerminalSize());
RerenderUtils.rerenderPart(
forStartX,
forEndX,
forStartY,
forEndY,
start.getX(),
start.getY(),
currentRoom,
room,
gameState.getPlayer(),
screenBuffer,
resourceManager,
debugging,
gameState.getOtherPlayers()
);
eventManager.emitEvent(
new RerenderScreen(new RerenderScreen.ScreenPart(
new TerminalPosition(forStartX, forStartY),
new TerminalPosition(forEndX * 2 + 1 + start.getX(), forEndY + start.getY())
))
);
}
}

View File

@@ -0,0 +1,60 @@
package cz.jzitnik.client.events.handlers;
import cz.jzitnik.client.annotations.EventHandler;
import cz.jzitnik.client.annotations.injectors.InjectDependency;
import cz.jzitnik.client.annotations.injectors.InjectState;
import cz.jzitnik.client.events.RoomChangeEvent;
import cz.jzitnik.client.events.SendSocketMessageEvent;
import cz.jzitnik.client.game.GameRoom;
import cz.jzitnik.client.game.GameState;
import cz.jzitnik.common.models.coordinates.RoomCords;
import cz.jzitnik.client.utils.events.AbstractEventHandler;
import cz.jzitnik.client.utils.events.EventManager;
import cz.jzitnik.client.utils.roomtasks.RoomTaskScheduler;
import cz.jzitnik.common.socket.messages.room.MovePlayerRoom;
import lombok.extern.slf4j.Slf4j;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;
@Slf4j
@EventHandler(RoomChangeEvent.class)
public class RoomChangeEventHandler extends AbstractEventHandler<RoomChangeEvent> {
private final ScheduledExecutorService scheduler = Executors.newScheduledThreadPool(1);
@InjectState
private GameState gameState;
@InjectDependency
private EventManager eventManager;
@InjectDependency
private RoomTaskScheduler roomTaskScheduler;
@Override
public void handle(RoomChangeEvent event) {
RoomCords playerCords = gameState.getPlayer().getPlayerCords();
RoomCords oldCords = playerCords.clone();
GameRoom currentRoom = gameState.getCurrentRoom();
GameRoom newRoom = switch (event.door()) {
case LEFT -> currentRoom.getLeft();
case RIGHT -> currentRoom.getRight();
case TOP -> currentRoom.getUp();
case BOTTOM -> currentRoom.getDown();
};
if (newRoom == null) {
return;
}
switch (event.door()) {
case LEFT -> playerCords.updateCords(155, playerCords.getY());
case RIGHT -> playerCords.updateCords(30, playerCords.getY());
case TOP -> playerCords.updateCords(playerCords.getX(), 110);
case BOTTOM -> playerCords.updateCords(playerCords.getX(), 10);
}
eventManager.emitEvent(new SendSocketMessageEvent(new MovePlayerRoom(newRoom.getId(), oldCords, playerCords)));
gameState.setCurrentRoom(newRoom);
scheduler.schedule(() -> roomTaskScheduler.setupNewSchedulers(newRoom), 200, TimeUnit.MILLISECONDS);
}
}

View File

@@ -0,0 +1,24 @@
package cz.jzitnik.client.events.handlers;
import cz.jzitnik.client.annotations.EventHandler;
import cz.jzitnik.client.annotations.injectors.InjectDependency;
import cz.jzitnik.client.events.SendSocketMessageEvent;
import cz.jzitnik.client.socket.Client;
import cz.jzitnik.client.utils.events.AbstractEventHandler;
import java.io.IOException;
@EventHandler(SendSocketMessageEvent.class)
public class SendSocketMessageEventHandler extends AbstractEventHandler<SendSocketMessageEvent> {
@InjectDependency
private Client client;
@Override
public void handle(SendSocketMessageEvent event) {
try {
client.send(event.message());
} catch (IOException e) {
throw new RuntimeException(e);
}
}
}

View File

@@ -0,0 +1,61 @@
package cz.jzitnik.client.events.handlers;
import com.googlecode.lanterna.TerminalSize;
import cz.jzitnik.client.annotations.EventHandler;
import cz.jzitnik.client.annotations.injectors.InjectDependency;
import cz.jzitnik.client.annotations.injectors.InjectState;
import cz.jzitnik.client.events.FullRoomDraw;
import cz.jzitnik.client.events.TerminalResizeEvent;
import cz.jzitnik.client.game.GameState;
import cz.jzitnik.client.states.ScreenBuffer;
import cz.jzitnik.client.ui.pixels.AlphaPixel;
import cz.jzitnik.client.ui.pixels.Empty;
import cz.jzitnik.client.ui.pixels.Pixel;
import cz.jzitnik.client.utils.events.AbstractEventHandler;
import cz.jzitnik.client.utils.events.EventManager;
import lombok.extern.slf4j.Slf4j;
@Slf4j
@EventHandler(TerminalResizeEvent.class)
public class TerminalResizeEventHandler extends AbstractEventHandler<TerminalResizeEvent> {
@InjectDependency
private EventManager eventManager;
@InjectState
private ScreenBuffer screenBuffer;
@InjectState
private GameState gameState;
private boolean screenRerendering = false;
@Override
public void handle(TerminalResizeEvent event) {
TerminalSize size = event.getNewSize();
int width = size.getColumns();
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) {
return;
} else {
screenRerendering = true;
gameState.getScreen().fullRender();
screenRerendering = false;
}
} else {
eventManager.emitEvent(new FullRoomDraw(true));
}
}
}

View File

@@ -0,0 +1,55 @@
package cz.jzitnik.client.events.handlers;
import com.googlecode.lanterna.SGR;
import com.googlecode.lanterna.TerminalSize;
import com.googlecode.lanterna.TextColor;
import com.googlecode.lanterna.screen.Screen;
import com.googlecode.lanterna.screen.TerminalScreen;
import cz.jzitnik.client.annotations.EventHandler;
import cz.jzitnik.client.annotations.injectors.InjectState;
import cz.jzitnik.client.events.TerminalTooSmallEvent;
import cz.jzitnik.client.states.TerminalState;
import cz.jzitnik.client.utils.events.AbstractEventHandler;
import java.io.IOException;
import java.util.EnumSet;
@EventHandler(TerminalTooSmallEvent.class)
public class TerminalTooSmallEventHandler extends AbstractEventHandler<TerminalTooSmallEvent> {
@InjectState
private TerminalState terminalState;
@Override
public void handle(TerminalTooSmallEvent event) {
// Directly render the message for the user
TerminalScreen terminalScreen = terminalState.getTerminalScreen();
terminalScreen.clear();
var tg = terminalState.getTextGraphics();
TerminalSize terminalSize = terminalScreen.getTerminalSize();
String text = "Terminal too small!";
String subText = "Please resize your terminal";
int startY = terminalSize.getRows() / 2;
int textStartX = (terminalSize.getColumns() / 2) - (text.length() / 2);
for (char character : text.toCharArray()) {
tg.setForegroundColor(TextColor.ANSI.RED);
tg.setBackgroundColor(TextColor.ANSI.DEFAULT);
tg.setModifiers(EnumSet.of(SGR.BOLD));
tg.setCharacter(textStartX++, startY, character);
}
int subTextStartX = (terminalSize.getColumns() / 2) - (subText.length() / 2);
for (char character : subText.toCharArray()) {
tg.setForegroundColor(TextColor.ANSI.BLACK_BRIGHT);
tg.setBackgroundColor(TextColor.ANSI.DEFAULT);
tg.setCharacter(subTextStartX++, startY + 1, character);
}
try {
terminalScreen.refresh(Screen.RefreshType.COMPLETE);
} catch (IOException e) {
throw new RuntimeException(e);
}
}
}

View File

@@ -0,0 +1,7 @@
package cz.jzitnik.client.game;
import com.googlecode.lanterna.TextColor;
public class Constants {
public static final TextColor BACKGROUND_COLOR = new TextColor.RGB(4, 4, 16);
}

View File

@@ -0,0 +1,10 @@
package cz.jzitnik.client.game;
import cz.jzitnik.common.models.coordinates.RoomCords;
import java.awt.image.BufferedImage;
public interface GamePlayer {
RoomCords getPlayerCords();
BufferedImage getTexture(ResourceManager resourceManager);
}

View File

@@ -0,0 +1,88 @@
package cz.jzitnik.client.game;
import com.fasterxml.jackson.annotation.*;
import cz.jzitnik.client.game.mobs.Mob;
import cz.jzitnik.client.game.objects.DroppedItem;
import cz.jzitnik.client.game.objects.GameObject;
import cz.jzitnik.client.ui.pixels.Empty;
import cz.jzitnik.client.ui.pixels.Pixel;
import cz.jzitnik.common.models.coordinates.RoomPart;
import lombok.Getter;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
@JsonIdentityInfo(
generator = ObjectIdGenerators.PropertyGenerator.class,
property = "id"
)
@Getter
public class GameRoom {
private final String id;
@JsonIgnore
private final Pixel[][] overrideBuffer;
@JsonIgnore
private final ResourceManager.Resource texture;
@JsonIgnore
private final List<GameObject> objects = new ArrayList<>();
@JsonIgnore
private final List<Mob> mobs = new ArrayList<>();
@JsonIgnore
private final Set<DroppedItem> droppedItems = new HashSet<>();
@JsonIgnore
private final List<RoomPart> colliders = new ArrayList<>();
private GameRoom left;
private GameRoom right;
private GameRoom up;
private GameRoom down;
@JsonCreator
public GameRoom(
@JsonProperty("id") String id,
@JsonProperty("objects") List<GameObject> objects,
@JsonProperty("colliders") List<RoomPart> colliders,
@JsonProperty("mobs") List<Mob> mobs,
@JsonProperty("texture") ResourceManager.Resource texture
) {
this.id = id;
this.texture = texture;
if (objects != null) this.objects.addAll(objects);
if (colliders != null) this.colliders.addAll(colliders);
if (mobs != null) this.mobs.addAll(mobs);
int height = 225;
int width = 225 * 2;
Pixel[][] overrideBuffer = new Pixel[height][width];
for (int x = 0; x < width; x++) {
for (int y = 0; y < height; y++) {
overrideBuffer[y][x] = new Empty();
}
}
this.overrideBuffer = overrideBuffer;
}
@JsonSetter("west")
public void setWest(GameRoom west) {
if (west != null) this.left = west;
}
@JsonSetter("east")
public void setEast(GameRoom east) {
if (east != null) this.right = east;
}
@JsonSetter("north")
public void setNorth(GameRoom north) {
if (north != null) this.up = north;
}
@JsonSetter("south")
public void setSouth(GameRoom south) {
if (south != null) this.down = south;
}
}

View File

@@ -0,0 +1,54 @@
package cz.jzitnik.client.game;
import cz.jzitnik.client.annotations.State;
import cz.jzitnik.client.game.objects.Interactable;
import cz.jzitnik.client.screens.Screen;
import cz.jzitnik.client.utils.DependencyManager;
import lombok.Getter;
import lombok.RequiredArgsConstructor;
import lombok.Setter;
import java.util.ArrayList;
import java.util.List;
@RequiredArgsConstructor
@State
public class GameState {
private final DependencyManager dependencyManager; // Maybe transient in the future
@Getter
@Setter
private GameRoom currentRoom;
@Getter
@Setter
private List<GameRoom> allRooms;
@Getter
@Setter
private Player player;
private final List<OtherPlayer> otherPlayers = new ArrayList<>();
public List<OtherPlayer> getOtherPlayers() {
return otherPlayers.stream().filter(OtherPlayer::isVisible).toList();
}
public List<OtherPlayer> getAllOtherPlayers() {
return otherPlayers;
}
@Getter
@Setter
private Interactable interacting;
@Getter
private Screen screen;
public void setScreen(Screen screen) {
if (screen != null) {
dependencyManager.inject(screen);
}
this.screen = screen;
}
}

View File

@@ -0,0 +1,41 @@
package cz.jzitnik.client.game;
import cz.jzitnik.client.game.mobs.HittableMob;
import cz.jzitnik.common.models.coordinates.RoomCords;
import cz.jzitnik.common.models.player.PlayerCreation;
import cz.jzitnik.common.socket.messages.player.PlayerRotation;
import lombok.Getter;
import lombok.Setter;
import java.awt.image.BufferedImage;
@Getter
public class OtherPlayer implements GamePlayer {
private final int id;
private boolean hitAnimationOn = false;
private final RoomCords playerCords;
@Setter
private PlayerRotation playerRotation = PlayerRotation.FRONT;
@Setter
private boolean visible;
public OtherPlayer(PlayerCreation playerCreation) {
this.id = playerCreation.getId();
this.playerCords = playerCreation.getPlayerCords();
}
public BufferedImage getTexture(ResourceManager resourceManager) {
BufferedImage resource = resourceManager.getResource(switch (playerRotation) {
case FRONT -> ResourceManager.Resource.PLAYER_FRONT;
case BACK -> ResourceManager.Resource.PLAYER_BACK;
case LEFT -> ResourceManager.Resource.PLAYER_LEFT;
case RIGHT -> ResourceManager.Resource.PLAYER_RIGHT;
});
if (hitAnimationOn) {
return HittableMob.applyRedFactor(resource);
}
return resource;
}
}

View File

@@ -0,0 +1,159 @@
package cz.jzitnik.client.game;
import cz.jzitnik.client.events.RerenderPart;
import cz.jzitnik.client.game.items.GameItem;
import cz.jzitnik.client.game.items.types.interfaces.WeaponInterface;
import cz.jzitnik.client.game.mobs.HittableMob;
import cz.jzitnik.common.models.coordinates.RoomPart;
import cz.jzitnik.common.models.coordinates.RoomCords;
import cz.jzitnik.client.ui.Inventory;
import cz.jzitnik.client.utils.DependencyManager;
import cz.jzitnik.client.utils.events.Event;
import cz.jzitnik.client.utils.events.EventManager;
import cz.jzitnik.common.models.player.PlayerCreation;
import cz.jzitnik.common.socket.messages.player.PlayerRotation;
import lombok.Getter;
import lombok.Setter;
import lombok.extern.slf4j.Slf4j;
import java.awt.image.BufferedImage;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.ScheduledFuture;
import java.util.concurrent.TimeUnit;
@Getter
@Slf4j
public class Player implements GamePlayer {
private final int id;
public static final int MAX_STAMINA = 20;
public static final int MAX_HEALTH = 30;
private final ScheduledExecutorService scheduler = Executors.newScheduledThreadPool(1);
private final RoomCords playerCords;
private final RoomPart collider;
private final GameItem[] inventory = new GameItem[Inventory.ITEMS_X * Inventory.ITEMS_Y];
@Setter
private PlayerRotation playerRotation = PlayerRotation.FRONT;
@Setter
private GameItem selectedItem;
private int health = MAX_HEALTH;
private int stamina = MAX_STAMINA;
private boolean swinging = false;
private boolean hitAnimationOn = false;
private ScheduledFuture<?> currentTimeoutHitAnimation = null;
public Player(PlayerCreation playerCreation) {
this.playerCords = playerCreation.getPlayerCords();
this.collider = playerCreation.getCollider();
this.id = playerCreation.getId();
}
public void increaseStamina() {
stamina++;
}
public void decreaseStamina() {
stamina--;
}
public void addHealth(int amount) {
health = Math.min(MAX_HEALTH, health + amount);
}
public boolean dealDamage(int amount, DependencyManager dependencyManager) {
if (health - amount <= 0) {
health = 0;
return true;
}
health -= amount;
if (hitAnimationOn) {
if (currentTimeoutHitAnimation != null && !currentTimeoutHitAnimation.isDone()) {
currentTimeoutHitAnimation.cancel(false);
}
} else {
hitAnimationOn = true;
rerender(dependencyManager);
}
currentTimeoutHitAnimation = scheduler.schedule(() -> {
hitAnimationOn = false;
rerender(dependencyManager);
}, 250, TimeUnit.MILLISECONDS);
return false;
}
private void rerender(DependencyManager dependencyManager) {
ResourceManager resourceManager = dependencyManager.getDependencyOrThrow(ResourceManager.class);
EventManager eventManager = dependencyManager.getDependencyOrThrow(EventManager.class);
int forStartX = playerCords.getX();
int forStartY = playerCords.getY();
BufferedImage playerTexture = getTexture(resourceManager);
int forEndX = playerCords.getX() + playerTexture.getWidth() - 1;
int forEndY = playerCords.getY() + playerTexture.getHeight();
eventManager.emitEvent(new Event[]{
new RerenderPart(forStartX, forEndX, forStartY, forEndY),
});
}
public BufferedImage getTexture(ResourceManager resourceManager) {
BufferedImage resource = resourceManager.getResource(switch (playerRotation) {
case FRONT -> ResourceManager.Resource.PLAYER_FRONT;
case BACK -> ResourceManager.Resource.PLAYER_BACK;
case LEFT -> ResourceManager.Resource.PLAYER_LEFT;
case RIGHT -> ResourceManager.Resource.PLAYER_RIGHT;
});
if (hitAnimationOn) {
return HittableMob.applyRedFactor(resource);
}
return resource;
}
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 != null && selectedItem.getType() instanceof WeaponInterface weapon) {
damage = weapon.getDamageDeal();
}
return damage;
}
public boolean addItem(GameItem item) {
boolean added = false;
for (int i = 0; i < inventory.length; i++) {
if (inventory[i] == null) {
inventory[i] = item;
added = true;
break;
}
}
return added;
}
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);
}
}

View File

@@ -0,0 +1,84 @@
package cz.jzitnik.client.game;
import cz.jzitnik.client.annotations.Dependency;
import cz.jzitnik.client.annotations.injectors.InjectDependency;
import lombok.AllArgsConstructor;
import lombok.Getter;
import javax.imageio.ImageIO;
import java.awt.image.BufferedImage;
import java.io.IOException;
import java.io.InputStream;
import java.util.HashMap;
@Dependency
public class ResourceManager {
@InjectDependency
private ClassLoader classLoader;
@AllArgsConstructor
@Getter
public enum Resource {
ROOM1("rooms/1.png"),
ROOM2("rooms/2.png"),
ROOM3("rooms/3.png"),
ROOM4("rooms/4.png"),
ROOM_FROZEN("rooms/frozen.png"),
PLAYER_FRONT("player/front.png"),
PLAYER_BACK("player/back.png"),
PLAYER_LEFT("player/left.png"),
PLAYER_RIGHT("player/right.png"),
CHEST("chest.png"),
WOODEN_SWORD("tools/wooden_sword.png"),
APPLE("food/apple.png"),
DOORS("rooms/doors.png"),
STAMINA("ui/stamina.png"),
HEART("ui/heart.png");
private final String path;
}
private final HashMap<Resource, BufferedImage> resourceCache = new HashMap<>();
public BufferedImage getResource(Resource resource) {
if (resourceCache.containsKey(resource)) {
return resourceCache.get(resource);
}
InputStream is = classLoader.getResourceAsStream("textures/" + resource.getPath());
if (is == null) {
throw new RuntimeException("Image not found in resources!");
}
try {
BufferedImage image = ImageIO.read(is);
resourceCache.put(resource, image);
return image;
} catch (IOException e) {
throw new RuntimeException(e);
}
}
public BufferedImage getResource(String path) {
InputStream is = classLoader.getResourceAsStream(path);
if (is == null) {
throw new RuntimeException("Image not found in resources!");
}
try {
return ImageIO.read(is);
} catch (IOException e) {
throw new RuntimeException(e);
}
}
public InputStream getResourceAsStream(String path) {
return classLoader.getResourceAsStream(path);
}
}

View File

@@ -0,0 +1,18 @@
package cz.jzitnik.client.game.dialog;
import cz.jzitnik.client.utils.events.Event;
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.RequiredArgsConstructor;
@AllArgsConstructor
@RequiredArgsConstructor
@Getter
public class Dialog implements Event {
/**
* Characters per second
*/
private int typingSpeed = 10;
private final String text;
private final OnEnd onEnd;
}

View File

@@ -0,0 +1,13 @@
package cz.jzitnik.client.game.dialog;
public interface OnEnd {
record RunCode(Runnable runnable, OnEnd onEnd) implements OnEnd {}
record Continue(Dialog nextDialog) implements OnEnd {
}
record AskQuestion(Answer[] answers) implements OnEnd {
public record Answer(String answer, Dialog dialog) {
}
}
}

View File

@@ -0,0 +1,11 @@
package cz.jzitnik.client.game.exceptions;
public class InvalidCoordinatesException extends RuntimeException {
public InvalidCoordinatesException(String message) {
super(message);
}
public InvalidCoordinatesException() {
super();
}
}

View File

@@ -0,0 +1,32 @@
package cz.jzitnik.client.game.items;
import com.fasterxml.jackson.annotation.*;
import cz.jzitnik.client.game.ResourceManager;
import cz.jzitnik.client.game.items.types.ItemType;
import cz.jzitnik.client.game.utils.Renderable;
import lombok.Getter;
import java.awt.image.BufferedImage;
@Getter
public class GameItem implements Renderable {
private final ItemType<?> type;
@JsonIgnore
private final BufferedImage texture;
private final String name;
private final int id;
@JsonCreator
public GameItem(
@JsonProperty("id") int id,
@JsonProperty("name") String name,
@JsonProperty("type") ItemType<?> type,
@JsonProperty("texture") ResourceManager.Resource resource,
@JacksonInject ResourceManager resourceManager
) {
this.id = id;
this.name = name;
this.type = type;
this.texture = resourceManager.getResource(resource);
}
}

View File

@@ -0,0 +1,12 @@
package cz.jzitnik.client.game.items.types;
import cz.jzitnik.client.utils.DependencyManager;
import cz.jzitnik.client.utils.StateManager;
public interface InteractableItem {
InteractableItemResponse interact(DependencyManager dependencyManager, StateManager stateManager);
enum InteractableItemResponse {
CLEAR_ITEM,
}
}

View File

@@ -0,0 +1,18 @@
package cz.jzitnik.client.game.items.types;
import com.fasterxml.jackson.annotation.JsonSubTypes;
import com.fasterxml.jackson.annotation.JsonTypeInfo;
import cz.jzitnik.client.game.items.types.food.Food;
import cz.jzitnik.client.game.items.types.weapons.Sword;
@JsonTypeInfo(
use = JsonTypeInfo.Id.NAME,
property = "name"
)
@JsonSubTypes({
@JsonSubTypes.Type(value = Food.class, name = "food"),
@JsonSubTypes.Type(value = Sword.class, name = "weapon_sword")
})
public interface ItemType<T> {
Class<T> getItemType();
}

View File

@@ -0,0 +1,38 @@
package cz.jzitnik.client.game.items.types.food;
import com.fasterxml.jackson.annotation.JsonCreator;
import com.fasterxml.jackson.annotation.JsonProperty;
import cz.jzitnik.client.events.RenderStats;
import cz.jzitnik.client.game.GameState;
import cz.jzitnik.client.game.items.types.InteractableItem;
import cz.jzitnik.client.game.items.types.ItemType;
import cz.jzitnik.client.utils.DependencyManager;
import cz.jzitnik.client.utils.StateManager;
import cz.jzitnik.client.utils.events.EventManager;
public class Food implements InteractableItem, ItemType<Food> {
private final int addHealth;
@JsonCreator
public Food(
@JsonProperty("addHealth") int addHealth
) {
this.addHealth = addHealth;
}
@Override
public InteractableItemResponse interact(DependencyManager dependencyManager, StateManager stateManager) {
GameState gameState = stateManager.getOrThrow(GameState.class);
EventManager eventManager = dependencyManager.getDependencyOrThrow(EventManager.class);
gameState.getPlayer().addHealth(addHealth);
eventManager.emitEvent(new RenderStats());
return InteractableItemResponse.CLEAR_ITEM;
}
@Override
public Class<Food> getItemType() {
return Food.class;
}
}

View File

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

View File

@@ -0,0 +1,13 @@
package cz.jzitnik.client.game.items.types.weapons;
import com.fasterxml.jackson.annotation.JsonCreator;
import com.fasterxml.jackson.annotation.JsonProperty;
public class Sword extends Weapon {
@JsonCreator
public Sword(
@JsonProperty("dealDamage") int dealDamage
) {
super(dealDamage);
}
}

View File

@@ -0,0 +1,20 @@
package cz.jzitnik.client.game.items.types.weapons;
import cz.jzitnik.client.game.items.types.ItemType;
import cz.jzitnik.client.game.items.types.interfaces.WeaponInterface;
import lombok.AllArgsConstructor;
@AllArgsConstructor
public abstract class Weapon implements ItemType<Weapon>, WeaponInterface {
private final int dealDamage;
@Override
public final Class<Weapon> getItemType() {
return Weapon.class;
}
@Override
public int getDamageDeal() {
return dealDamage;
}
}

View File

@@ -0,0 +1,37 @@
package cz.jzitnik.client.game.mobs;
import cz.jzitnik.client.annotations.injectors.InjectDependency;
import cz.jzitnik.client.annotations.injectors.InjectState;
import cz.jzitnik.common.models.coordinates.RoomPart;
import cz.jzitnik.client.game.dialog.Dialog;
import cz.jzitnik.client.game.mobs.tasks.MobRoomTask;
import cz.jzitnik.common.models.coordinates.RoomCords;
import cz.jzitnik.client.states.DialogState;
import cz.jzitnik.client.utils.events.EventManager;
import lombok.extern.slf4j.Slf4j;
import java.awt.image.BufferedImage;
@Slf4j
public abstract class DialogMob extends Mob {
protected Dialog dialog;
public DialogMob(BufferedImage texture, MobRoomTask[] tasks, RoomCords cords, RoomPart collider, Dialog dialog) {
super(texture, tasks, cords, collider);
this.dialog = dialog;
}
@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);
}
}
}

View File

@@ -0,0 +1,122 @@
package cz.jzitnik.client.game.mobs;
import cz.jzitnik.client.annotations.injectors.InjectDependency;
import cz.jzitnik.client.annotations.injectors.InjectState;
import cz.jzitnik.client.events.RerenderPart;
import cz.jzitnik.common.models.coordinates.RoomPart;
import cz.jzitnik.client.game.GameState;
import cz.jzitnik.client.game.mobs.tasks.MobRoomTask;
import cz.jzitnik.common.models.coordinates.RoomCords;
import cz.jzitnik.client.utils.events.EventManager;
import cz.jzitnik.client.utils.roomtasks.RoomTask;
import cz.jzitnik.client.utils.roomtasks.RoomTaskScheduler;
import lombok.Getter;
import lombok.extern.slf4j.Slf4j;
import java.awt.image.BufferedImage;
import java.awt.image.DataBufferByte;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.ScheduledFuture;
import java.util.concurrent.TimeUnit;
@Slf4j
public abstract class HittableMob extends Mob {
public abstract void onKilled();
private final ScheduledExecutorService scheduler = Executors.newSingleThreadScheduledExecutor();
@Getter
protected int health;
private ScheduledFuture<?> currentTimeoutHitAnimation = null;
private boolean hitAnimationOn = false;
@Override
public BufferedImage getTexture() {
if (hitAnimationOn) {
return applyRedFactor(texture);
}
return texture;
}
public static BufferedImage applyRedFactor(BufferedImage src) {
final float redFactor = 2f;
int width = src.getWidth();
int height = src.getHeight();
BufferedImage copy = new BufferedImage(width, height, BufferedImage.TYPE_4BYTE_ABGR);
byte[] srcPixels = ((DataBufferByte) src.getRaster().getDataBuffer()).getData();
byte[] dstPixels = ((DataBufferByte) copy.getRaster().getDataBuffer()).getData();
System.arraycopy(srcPixels, 0, dstPixels, 0, srcPixels.length);
for (int i = 0; i < dstPixels.length; i += 4) {
int red = dstPixels[i + 3] & 0xFF;
red = (int) (red * redFactor);
if (red > 255) red = 255;
dstPixels[i + 3] = (byte) red;
}
return copy;
}
@InjectDependency
private EventManager eventManager;
@InjectState
private GameState gameState;
@InjectDependency
private RoomTaskScheduler roomTaskScheduler;
public HittableMob(BufferedImage texture, MobRoomTask[] tasks, RoomCords cords, RoomPart collider, int health) {
super(texture, tasks, cords, collider);
this.health = health;
}
@Override
public final void interact() {
health -= gameState.getPlayer().getDamageDeal();
log.debug("Health: {}", health);
if (health <= 0) {
onKilled();
for (RoomTask task : tasks) {
roomTaskScheduler.stopTask(task);
}
gameState.getCurrentRoom().getMobs().remove(this);
rerender();
return;
}
if (hitAnimationOn) {
if (currentTimeoutHitAnimation != null && !currentTimeoutHitAnimation.isDone()) {
currentTimeoutHitAnimation.cancel(false);
}
} else {
hitAnimationOn = true;
log.debug("Hitting start");
rerender();
}
currentTimeoutHitAnimation = scheduler.schedule(() -> {
hitAnimationOn = false;
log.debug("Hitting end");
rerender();
}, 250, TimeUnit.MILLISECONDS);
}
private void rerender() {
int forStartX = cords.getX();
int forStartY = cords.getY();
int forEndX = cords.getX() + texture.getWidth() - 1;
int forEndY = cords.getY() + texture.getHeight();
eventManager.emitEvent(new RerenderPart(
forStartX,
forEndX,
forStartY,
forEndY
));
}
}

View File

@@ -0,0 +1,87 @@
package cz.jzitnik.client.game.mobs;
import com.fasterxml.jackson.annotation.JacksonInject;
import com.fasterxml.jackson.annotation.JsonCreator;
import com.fasterxml.jackson.annotation.JsonProperty;
import cz.jzitnik.client.annotations.injectors.InjectDependency;
import cz.jzitnik.client.annotations.injectors.InjectState;
import cz.jzitnik.client.events.DroppedItemRerender;
import cz.jzitnik.client.events.InventoryRerender;
import cz.jzitnik.client.game.*;
import cz.jzitnik.client.game.items.GameItem;
import cz.jzitnik.client.game.mobs.tasks.MobRoomTask;
import cz.jzitnik.client.game.objects.DroppedItem;
import cz.jzitnik.common.models.coordinates.RoomPart;
import cz.jzitnik.common.models.coordinates.RoomCords;
import cz.jzitnik.client.utils.events.Event;
import cz.jzitnik.client.utils.events.EventManager;
import java.awt.image.BufferedImage;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.ThreadLocalRandom;
public class HittableMobDrops extends HittableMob {
private static final int DROP_ITEM_ON_GROUND_RADIUS = 30;
private final GameItem[] itemsDrops;
@InjectState
private GameState gameState;
@InjectDependency
private EventManager eventManager;
@JsonCreator
public HittableMobDrops(
@JsonProperty("texture") ResourceManager.Resource texture,
@JsonProperty("tasks") MobRoomTask[] tasks,
@JsonProperty("cords") RoomCords cords,
@JsonProperty("collider") RoomPart collider,
@JsonProperty("health") int health,
@JsonProperty("itemsDrops") GameItem[] itemsDrops,
@JacksonInject ResourceManager resourceManager
) {
super(resourceManager.getResource(texture), tasks, cords, collider, health);
this.itemsDrops = itemsDrops == null ? new GameItem[]{} : itemsDrops;
}
/**
* Can be overwritten by an extending class
**/
public void afterKill() {
}
@Override
public final void onKilled() {
boolean addedIntoInventory = false;
Player player = gameState.getPlayer();
RoomCords enemyCords = getCords();
BufferedImage enemyTexture = getTexture();
GameRoom currentRoom = gameState.getCurrentRoom();
int roomX = enemyCords.getX() + enemyTexture.getWidth() / 2;
int roomY = enemyCords.getY() + enemyTexture.getHeight() / 2;
List<Event> events = new ArrayList<>();
for (GameItem item : itemsDrops) {
boolean added = player.addItem(item);
if (added) {
if (!addedIntoInventory) {
addedIntoInventory = true;
events.add(new InventoryRerender());
}
} else {
double angle = ThreadLocalRandom.current().nextDouble(0, Math.PI * 2);
double radius = ThreadLocalRandom.current().nextDouble(0, DROP_ITEM_ON_GROUND_RADIUS);
int randomX = roomX + (int) (Math.cos(angle) * radius);
int randomY = roomY + (int) (Math.sin(angle) * radius);
RoomCords itemCords = new RoomCords(randomX, randomY);
DroppedItem droppedItem = new DroppedItem(currentRoom, itemCords, item);
currentRoom.getDroppedItems().add(droppedItem);
events.add(new DroppedItemRerender(droppedItem));
}
}
eventManager.emitEvent(events, this::afterKill);
}
}

View File

@@ -0,0 +1,28 @@
package cz.jzitnik.client.game.mobs;
import com.fasterxml.jackson.annotation.JacksonInject;
import com.fasterxml.jackson.annotation.JsonCreator;
import com.fasterxml.jackson.annotation.JsonProperty;
import cz.jzitnik.common.models.coordinates.RoomPart;
import cz.jzitnik.client.game.ResourceManager;
import cz.jzitnik.client.game.mobs.tasks.MobRoomTask;
import cz.jzitnik.common.models.coordinates.RoomCords;
public class HittableMobNoDrops extends HittableMob {
@JsonCreator
public HittableMobNoDrops(
@JsonProperty("texture") ResourceManager.Resource texture,
@JsonProperty("tasks") MobRoomTask[] tasks,
@JsonProperty("cords") RoomCords cords,
@JsonProperty("collider") RoomPart collider,
@JsonProperty("health") int health,
@JacksonInject ResourceManager resourceManager
) {
super(resourceManager.getResource(texture), tasks, cords, collider, health);
}
@Override
public void onKilled() {
}
}

View File

@@ -0,0 +1,64 @@
package cz.jzitnik.client.game.mobs;
import com.fasterxml.jackson.annotation.JsonIgnore;
import com.fasterxml.jackson.annotation.JsonSubTypes;
import com.fasterxml.jackson.annotation.JsonTypeInfo;
import cz.jzitnik.client.annotations.injectors.InjectDependency;
import cz.jzitnik.common.models.coordinates.RoomPart;
import cz.jzitnik.client.game.mobs.tasks.MobRoomTask;
import cz.jzitnik.client.game.utils.Renderable;
import cz.jzitnik.common.models.coordinates.RoomCords;
import cz.jzitnik.client.game.utils.Selectable;
import cz.jzitnik.client.utils.roomtasks.RoomTaskScheduler;
import lombok.Getter;
import lombok.Setter;
import java.awt.image.BufferedImage;
@JsonTypeInfo(
use = JsonTypeInfo.Id.NAME,
property = "type"
)
@JsonSubTypes({
@JsonSubTypes.Type(value = DialogMob.class, name = "dialog"),
@JsonSubTypes.Type(value = HittableMobDrops.class, name = "hittable_drops"),
@JsonSubTypes.Type(value = HittableMobNoDrops.class, name = "hittable_no_drops")
})
@Getter
public abstract class Mob implements Renderable, Selectable {
@JsonIgnore
protected final BufferedImage texture;
@JsonIgnore
protected MobRoomTask[] tasks;
@JsonIgnore
protected final RoomCords cords;
@JsonIgnore
protected final RoomPart collider;
@InjectDependency
private RoomTaskScheduler roomTaskScheduler;
protected void updateTasks(MobRoomTask[] tasks) {
var oldTasks = this.tasks;
this.tasks = tasks;
roomTaskScheduler.registerNewMob(this, oldTasks);
}
public Mob(BufferedImage texture, MobRoomTask[] tasks, RoomCords cords, RoomPart collider) {
this.texture = texture;
this.tasks = tasks == null ? new MobRoomTask[] {} : tasks;
this.cords = cords;
this.collider = collider;
if (tasks != null) {
for (MobRoomTask task : tasks) {
task.setOwner(this);
}
}
}
@Setter
private boolean selected = false;
}

View File

@@ -0,0 +1,80 @@
package cz.jzitnik.client.game.mobs.tasks;
import com.fasterxml.jackson.annotation.JsonCreator;
import com.fasterxml.jackson.annotation.JsonProperty;
import cz.jzitnik.client.annotations.injectors.InjectConfig;
import cz.jzitnik.client.annotations.injectors.InjectDependency;
import cz.jzitnik.client.annotations.injectors.InjectState;
import cz.jzitnik.client.config.Debugging;
import cz.jzitnik.client.config.MicrophoneConfig;
import cz.jzitnik.client.game.GameState;
import cz.jzitnik.client.game.ResourceManager;
import cz.jzitnik.client.game.mobs.Mob;
import cz.jzitnik.common.models.coordinates.RoomCords;
import cz.jzitnik.client.states.MicrophoneState;
import cz.jzitnik.client.states.ScreenBuffer;
import cz.jzitnik.client.states.TerminalState;
import cz.jzitnik.client.utils.events.EventManager;
import lombok.RequiredArgsConstructor;
import lombok.Setter;
import java.util.concurrent.TimeUnit;
public class BlindMobFollowingPlayerTask extends MobRoomTask {
private final Task task;
@JsonCreator
public BlindMobFollowingPlayerTask(
@JsonProperty("speed") int speed,
@JsonProperty("updateRateMs") int updateRateMs
) {
Task task = new Task(speed);
super(task, updateRateMs, TimeUnit.MILLISECONDS);
this.task = task;
}
@Override
public void setOwner(Mob mob) {
task.setMob(mob);
}
@RequiredArgsConstructor
private static class Task implements Runnable {
@Setter
private Mob mob;
private final int speed;
private RoomCords playerCords;
@InjectState
private GameState gameState;
@InjectDependency
private EventManager eventManager;
@InjectDependency
private ResourceManager resourceManager;
@InjectState
private TerminalState terminalState;
@InjectState
private ScreenBuffer screenBuffer;
@InjectConfig
private Debugging debugging;
@InjectState
private MicrophoneState microphoneState;
@InjectConfig
private MicrophoneConfig microphoneConfig;
@Override
public void run() {
if (playerCords == null || (microphoneState.isMicrophoneSetup() && microphoneState.getMicrophoneVolume() > microphoneConfig.volumeThreshold())) {
playerCords = gameState.getPlayer().getPlayerCords().clone();
}
MobFollowingPlayerTask.Task.moveMob(playerCords, mob, gameState, speed, resourceManager, terminalState, screenBuffer, debugging, eventManager);
}
}
}

View File

@@ -0,0 +1,72 @@
package cz.jzitnik.client.game.mobs.tasks;
import com.fasterxml.jackson.annotation.JsonCreator;
import com.fasterxml.jackson.annotation.JsonProperty;
import cz.jzitnik.client.annotations.injectors.InjectDependency;
import cz.jzitnik.client.annotations.injectors.InjectState;
import cz.jzitnik.client.events.RenderStats;
import cz.jzitnik.client.game.GameState;
import cz.jzitnik.client.game.mobs.Mob;
import cz.jzitnik.common.models.coordinates.RoomCords;
import cz.jzitnik.client.utils.DependencyManager;
import cz.jzitnik.client.utils.events.EventManager;
import lombok.RequiredArgsConstructor;
import lombok.Setter;
import lombok.extern.slf4j.Slf4j;
import java.util.concurrent.TimeUnit;
@Slf4j
public class EnemyPlayerAttackingTask extends MobRoomTask {
private final Task task;
@JsonCreator
public EnemyPlayerAttackingTask(
@JsonProperty("updateRateMs") long updateRateMs,
@JsonProperty("reach") double reach,
@JsonProperty("damage") int damage
) {
Task task = new Task(reach, damage);
super(task, updateRateMs, TimeUnit.MILLISECONDS);
this.task = task;
}
@Override
public void setOwner(Mob mob) {
task.setMob(mob);
}
@RequiredArgsConstructor
private static class Task implements Runnable {
private final double reach;
private final int damage;
@Setter
private Mob mob;
@InjectState
private GameState gameState;
@InjectDependency
private EventManager eventManager;
@InjectDependency
private DependencyManager dependencyManager;
@Override
public void run() {
RoomCords playerCords = gameState.getPlayer().getPlayerCords();
RoomCords mobCords = mob.getCords();
double distance = playerCords.calculateDistance(mobCords);
if (distance > reach) {
return;
}
boolean isDead = gameState.getPlayer().dealDamage(damage, dependencyManager);
eventManager.emitEvent(new RenderStats());
log.debug("Is dead: {}", isDead);
// TODO: Death screen
}
}
}

View File

@@ -0,0 +1,120 @@
package cz.jzitnik.client.game.mobs.tasks;
import com.fasterxml.jackson.annotation.JsonCreator;
import com.fasterxml.jackson.annotation.JsonProperty;
import com.googlecode.lanterna.TerminalPosition;
import cz.jzitnik.client.annotations.injectors.InjectConfig;
import cz.jzitnik.client.annotations.injectors.InjectDependency;
import cz.jzitnik.client.annotations.injectors.InjectState;
import cz.jzitnik.client.config.Debugging;
import cz.jzitnik.client.events.MouseMoveEvent;
import cz.jzitnik.client.events.RerenderScreen;
import cz.jzitnik.common.models.coordinates.RoomPart;
import cz.jzitnik.client.game.GameState;
import cz.jzitnik.client.game.Player;
import cz.jzitnik.client.game.ResourceManager;
import cz.jzitnik.client.game.mobs.Mob;
import cz.jzitnik.client.game.mobs.tasks.utils.AStarAlg;
import cz.jzitnik.common.models.coordinates.RoomCords;
import cz.jzitnik.client.states.ScreenBuffer;
import cz.jzitnik.client.states.TerminalState;
import cz.jzitnik.client.utils.RerenderUtils;
import cz.jzitnik.client.utils.events.Event;
import cz.jzitnik.client.utils.events.EventManager;
import lombok.RequiredArgsConstructor;
import lombok.Setter;
import lombok.extern.slf4j.Slf4j;
import java.awt.image.BufferedImage;
import java.util.List;
import java.util.concurrent.TimeUnit;
@Slf4j
public class MobFollowingPlayerTask extends MobRoomTask {
private final Task task;
@JsonCreator
public MobFollowingPlayerTask(
@JsonProperty("speed") int speed,
@JsonProperty("updateRateMs") int updateRateMs
) {
Task task = new Task(speed);
super(task, updateRateMs, TimeUnit.MILLISECONDS);
this.task = task;
}
@Override
public void setOwner(Mob mob) {
task.setMob(mob);
}
@RequiredArgsConstructor
static class Task implements Runnable {
private final int speed;
@Setter
private Mob mob;
@InjectState
private GameState gameState;
@InjectDependency
private EventManager eventManager;
@InjectDependency
private ResourceManager resourceManager;
@InjectState
private TerminalState terminalState;
@InjectState
private ScreenBuffer screenBuffer;
@InjectConfig
private Debugging debugging;
protected static void moveMob(RoomCords playerCords, Mob mob, GameState gameState, int speed, ResourceManager resourceManager, TerminalState terminalState, ScreenBuffer screenBuffer, Debugging debugging, EventManager eventManager) {
RoomCords mobCords = mob.getCords();
List<RoomPart> solidParts = gameState.getCurrentRoom().getColliders();
List<RoomCords> path = AStarAlg.findPath(mobCords, playerCords, solidParts, mob.getCollider());
if (path.size() > 1) {
int targetIndex = Math.min(speed, path.size() - 1);
RoomCords newCords = path.get(targetIndex);
mob.getCords().updateCords(newCords.getX(), newCords.getY());
int forStartX = Math.min(mobCords.getX(), newCords.getX());
int forStartY = Math.min(mobCords.getY(), newCords.getY());
int forEndX = Math.max(mobCords.getX(), newCords.getX()) + mob.getTexture().getWidth() + 1;
int forEndY = Math.max(mobCords.getY(), newCords.getY()) + mob.getTexture().getHeight() + 1;
BufferedImage room = resourceManager.getResource(gameState.getCurrentRoom().getTexture());
var start = RerenderUtils.getStart(room, terminalState.getTerminalScreen().getTerminalSize());
int startX = start.getX();
int startY = start.getY();
Player player = gameState.getPlayer();
BufferedImage playerTexture = player.getTexture(resourceManager);
RerenderUtils.rerenderPart(forStartX, forEndX, forStartY, forEndY, startX, startY, gameState.getCurrentRoom(), room, player, screenBuffer, resourceManager, debugging, gameState.getOtherPlayers());
eventManager.emitEvent(new Event[]{
new MouseMoveEvent(null),
new RerenderScreen(
new RerenderScreen.ScreenPart(
new TerminalPosition(forStartX + startX, forStartY + startY),
new TerminalPosition(forEndX + 1 + startX, forEndY + startY)
)
)
});
} else {
log.debug("Mob is effectively at the target or trapped.");
}
}
@Override
public void run() {
moveMob(gameState.getPlayer().getPlayerCords(), mob, gameState, speed, resourceManager, terminalState, screenBuffer, debugging, eventManager);
}
}
}

View File

@@ -0,0 +1,26 @@
package cz.jzitnik.client.game.mobs.tasks;
import com.fasterxml.jackson.annotation.JsonSubTypes;
import com.fasterxml.jackson.annotation.JsonTypeInfo;
import cz.jzitnik.client.game.mobs.Mob;
import cz.jzitnik.client.utils.roomtasks.RoomTask;
import java.util.concurrent.TimeUnit;
@JsonTypeInfo(
use = JsonTypeInfo.Id.NAME,
property = "type"
)
@JsonSubTypes({
@JsonSubTypes.Type(value = BlindMobFollowingPlayerTask.class, name = "blind_following_player"),
@JsonSubTypes.Type(value = MobFollowingPlayerTask.class, name = "following_player"),
@JsonSubTypes.Type(value = EnemyPlayerAttackingTask.class, name = "attacking_player")
})
public abstract class MobRoomTask extends RoomTask {
public MobRoomTask(Runnable task, long rate, TimeUnit rateUnit) {
super(task, rate, rateUnit);
}
public abstract void setOwner(Mob mob);
}

View File

@@ -0,0 +1,119 @@
package cz.jzitnik.client.game.mobs.tasks.utils;
import cz.jzitnik.common.models.coordinates.RoomPart;
import cz.jzitnik.common.models.coordinates.RoomCords;
import java.util.*;
public class AStarAlg {
private static final int MIN_X = 30;
private static final int MAX_X = 155;
private static final int MIN_Y = 10;
private static final int MAX_Y = 113;
public static List<RoomCords> findPath(RoomCords start, RoomCords target, List<RoomPart> colliders, RoomPart mobCollider) {
PriorityQueue<Node> openSet = new PriorityQueue<>(Comparator.comparingInt(n -> n.f));
Set<String> closedSet = new HashSet<>();
Node startNode = new Node(start.getX(), start.getY(), 0, getHeuristic(start, target), null);
openSet.add(startNode);
while (!openSet.isEmpty()) {
Node current = openSet.poll();
if (current.x == target.getX() && current.y == target.getY()) {
return reconstructPath(current);
}
String key = current.x + "," + current.y;
if (closedSet.contains(key)) continue;
closedSet.add(key);
for (Node neighbor : getNeighbors(current, target)) {
String neighborKey = neighbor.x + "," + neighbor.y;
if (closedSet.contains(neighborKey)) continue;
if (!isValidPosition(neighbor.x, neighbor.y, colliders, mobCollider)) {
if (neighbor.x != target.getX() || neighbor.y != target.getY()) {
continue;
}
}
int newG = current.g + 1;
neighbor.g = newG;
neighbor.f = newG + neighbor.h;
neighbor.parent = current;
openSet.add(neighbor);
}
}
return new ArrayList<>();
}
private static List<Node> getNeighbors(Node current, RoomCords target) {
List<Node> neighbors = new ArrayList<>();
int[][] directions = {
{0, 1}, {0, -1}, {1, 0}, {-1, 0}, // Up, Down, Right, Left
{1, 1}, {1, -1}, {-1, 1}, {-1, -1} // Diagonals
};
for (int[] dir : directions) {
int newX = current.x + dir[0];
int newY = current.y + dir[1];
int h = getHeuristic(new RoomCords(newX, newY), target);
neighbors.add(new Node(newX, newY, 0, h, null));
}
return neighbors;
}
private static boolean isValidPosition(int x, int y, List<RoomPart> colliders, RoomPart mobCollider) {
if (x < MIN_X || x > MAX_X) return false;
if (y < MIN_Y || y > MAX_Y) return false;
var temp = new RoomPart(
new RoomCords(mobCollider.getStart().getX() + x, mobCollider.getStart().getY() + y),
new RoomCords(mobCollider.getEnd().getX() + x, mobCollider.getEnd().getY() + y)
);
for (RoomPart part : colliders) {
if (part.isOverlapping(temp)) {
return false;
}
}
return true;
}
private static List<RoomCords> reconstructPath(Node endNode) {
List<RoomCords> path = new ArrayList<>();
Node current = endNode;
while (current != null) {
path.add(new RoomCords(current.x, current.y));
current = current.parent;
}
Collections.reverse(path);
return path;
}
private static int getHeuristic(RoomCords a, RoomCords b) {
int dx = Math.abs(a.getX() - b.getX());
int dy = Math.abs(a.getY() - b.getY());
return Math.max(dx, dy);
}
private static class Node {
int x, y;
int g, h, f;
Node parent;
public Node(int x, int y, int g, int h, Node parent) {
this.x = x;
this.y = y;
this.g = g;
this.h = h;
this.f = g + h;
this.parent = parent;
}
}
}

View File

@@ -0,0 +1,283 @@
package cz.jzitnik.client.game.objects;
import com.fasterxml.jackson.annotation.JacksonInject;
import com.fasterxml.jackson.annotation.JsonCreator;
import com.fasterxml.jackson.annotation.JsonProperty;
import com.google.common.collect.Lists;
import com.googlecode.lanterna.TerminalPosition;
import com.googlecode.lanterna.TextColor;
import cz.jzitnik.client.annotations.injectors.InjectConfig;
import cz.jzitnik.client.annotations.injectors.InjectDependency;
import cz.jzitnik.client.annotations.injectors.InjectState;
import cz.jzitnik.client.config.Debugging;
import cz.jzitnik.client.events.*;
import cz.jzitnik.client.game.GameRoom;
import cz.jzitnik.client.game.GameState;
import cz.jzitnik.client.game.Player;
import cz.jzitnik.client.game.ResourceManager;
import cz.jzitnik.client.game.items.GameItem;
import cz.jzitnik.common.models.coordinates.RoomCords;
import cz.jzitnik.client.states.ScreenBuffer;
import cz.jzitnik.client.states.TerminalState;
import cz.jzitnik.client.ui.Inventory;
import cz.jzitnik.client.ui.utils.Grid;
import cz.jzitnik.client.ui.pixels.ColoredPixel;
import cz.jzitnik.client.ui.pixels.Empty;
import cz.jzitnik.client.ui.pixels.Pixel;
import cz.jzitnik.client.utils.RerenderUtils;
import cz.jzitnik.client.utils.UIRoomClickHandlerRepository;
import cz.jzitnik.client.utils.events.EventManager;
import cz.jzitnik.common.socket.messages.items.ItemTookFromChest;
import lombok.Getter;
import lombok.extern.slf4j.Slf4j;
import java.awt.image.BufferedImage;
import java.net.Socket;
import java.util.HashSet;
import java.util.List;
@Slf4j
public final class Chest extends GameObject implements UIClickHandler {
private static final int RENDER_PADDING = 1;
@Getter
private final List<GameItem> items;
private boolean rendered;
private int listenerHashCode;
private int actualDisplayStartX;
private int actualDisplayStartY;
private int chestUISizeX;
private int chestUISizeY;
@InjectDependency
private ResourceManager resourceManager;
@InjectDependency
private EventManager eventManager;
@InjectDependency
private UIRoomClickHandlerRepository uiRoomClickHandlerRepository;
@InjectState
private GameState gameState;
@InjectState
private ScreenBuffer screenBuffer;
@InjectState
private TerminalState terminalState;
@InjectConfig
private Debugging debugging;
@JsonCreator
public Chest(
@JsonProperty("cords") RoomCords cords,
@JsonProperty("items") GameItem[] items,
@JacksonInject ResourceManager resourceManager
) {
super(resourceManager.getResource(ResourceManager.Resource.CHEST), cords, true);
this.items = Lists.newArrayList(items == null ? new GameItem[] {} : items);
}
@Override
public void interact() {
log.debug("Interacted with chest");
render(false);
}
private Grid createGrid(int itemCount) {
return new Grid(
Math.max(1, itemCount), // Items X
1, // Items Y
2, // Outer Border
2, // Inner Border
16, // Item Size
1, // Item Padding (Changed to 1 as per your request example)
Inventory.BORDER_COLOR,
Inventory.BACKGROUND_COLOR
);
}
private void render(boolean clear) {
GameRoom currentRoom = gameState.getCurrentRoom();
Player player = gameState.getPlayer();
BufferedImage roomTexture = resourceManager.getResource(currentRoom.getTexture());
BufferedImage chestTexture = getTexture();
var buffer = screenBuffer.getRenderedBuffer();
var overrideBuffer = currentRoom.getOverrideBuffer();
RoomCords start = RerenderUtils.getStart(
roomTexture,
terminalState.getTerminalScreen().getTerminalSize()
);
Grid currentGrid = createGrid(items.size());
this.chestUISizeX = currentGrid.getWidth();
this.chestUISizeY = currentGrid.getHeight();
int chestUIStartX = getCords().getX();
int chestUIStartY = getCords().getY();
int guiStartX = chestUIStartX + (chestTexture.getWidth() / 2) - (chestUISizeX / 2);
int guiStartY = chestUIStartY - chestUISizeY - 1;
actualDisplayStartX = guiStartX + start.getX();
actualDisplayStartY = guiStartY + start.getY();
int renderMinX = actualDisplayStartX;
int renderMinY = actualDisplayStartY;
int renderMaxX = actualDisplayStartX + chestUISizeX;
int renderMaxY = actualDisplayStartY + chestUISizeY;
if (clear) {
Grid previousGrid = createGrid(items.size() + 1);
int prevWidth = previousGrid.getWidth();
int prevGuiStartX = chestUIStartX + (chestTexture.getWidth() / 2) - (prevWidth / 2);
int prevDisplayStartX = prevGuiStartX + start.getX();
renderMinX = Math.min(renderMinX, prevDisplayStartX);
renderMaxX = Math.max(renderMaxX, prevDisplayStartX + prevWidth);
clearPreviousUI(
currentRoom, roomTexture, player, resourceManager,
buffer, overrideBuffer, start,
guiStartY,
prevGuiStartX,
prevWidth,
chestUISizeY
);
}
TerminalPosition guiStart = new TerminalPosition(renderMinX - RENDER_PADDING, renderMinY - RENDER_PADDING);
TerminalPosition guiEnd = new TerminalPosition(renderMaxX + RENDER_PADDING, renderMaxY + RENDER_PADDING);
RerenderScreen.ScreenPart sp = new RerenderScreen.ScreenPart(guiStart, guiEnd);
if (!items.isEmpty()) {
drawUI(currentGrid, buffer, overrideBuffer, start, guiStartX, guiStartY);
listenerHashCode = uiRoomClickHandlerRepository.registerCurrentRoomHandler(sp, this);
}
eventManager.emitEvent(new RerenderPart(
guiStart.getColumn() - start.getX(),
guiEnd.getColumn() - start.getX(),
guiStart.getRow() - start.getY(),
guiEnd.getRow() - start.getY()
));
rendered = true;
}
private void clearPreviousUI(
GameRoom room,
BufferedImage roomTexture,
Player player,
ResourceManager resourceManager,
Pixel[][] buffer,
Pixel[][] overrideBuffer,
RoomCords start,
int guiStartY,
int clearStartX,
int clearWidth,
int clearHeight
) {
for (int y = guiStartY; y < guiStartY + clearHeight; y++) {
for (int x = clearStartX; x < clearStartX + clearWidth; x++) {
int pixel = RerenderUtils.getPixel(
room,
roomTexture,
null,
new HashSet<>(),
player,
resourceManager,
x,
y,
debugging,
gameState.getOtherPlayers()
).pixel();
buffer[y + start.getY()][x + start.getX()] = pixelToColored(pixel);
overrideBuffer[y][x] = new Empty();
}
}
}
private void drawUI(
Grid grid,
Pixel[][] buffer,
Pixel[][] overrideBuffer,
RoomCords start,
int guiStartX,
int guiStartY
) {
BufferedImage[] textures = items.stream()
.map(GameItem::getTexture)
.toArray(BufferedImage[]::new);
Pixel[][] uiPixels = grid.render(textures);
for (int y = 0; y < grid.getHeight(); y++) {
for (int x = 0; x < grid.getWidth(); x++) {
Pixel pixel = uiPixels[y][x];
int targetY = guiStartY + y + start.getY();
int targetX = guiStartX + x + start.getX();
if (targetY >= 0 && targetY < buffer.length && targetX >= 0 && targetX < buffer[0].length) {
buffer[targetY][targetX] = pixel;
overrideBuffer[guiStartY + y][guiStartX + x] = pixel;
}
}
}
}
private Pixel pixelToColored(int argb) {
int r = (argb >> 16) & 0xff;
int g = (argb >> 8) & 0xff;
int b = argb & 0xff;
return new ColoredPixel(new TextColor.RGB(r, g, b));
}
@Override
public boolean handleClick(MouseAction mouseAction) {
int mouseX = mouseAction.getPosition().getColumn();
int mouseY = mouseAction.getPosition().getRow();
int localX = mouseX - actualDisplayStartX;
int localY = (mouseY * 2) - actualDisplayStartY;
Grid grid = createGrid(items.size());
int itemIndex = grid.getItemIndexAt(localX, localY);
if (itemIndex == -1 || itemIndex >= items.size()) {
return false;
}
GameItem item = items.get(itemIndex);
boolean added = gameState.getPlayer().addItem(item);
if (!added) {
return true;
}
eventManager.emitEvent(new SendSocketMessageEvent(new ItemTookFromChest(gameState.getCurrentRoom().getId(), item.getId())));
return true;
}
public void handleItemRemoval(GameItem item) {
items.remove(item);
if (!rendered) {
return;
}
eventManager.emitEvent(new InventoryRerender());
if (items.isEmpty()) {
uiRoomClickHandlerRepository.removeHandlerForCurrentRoom(listenerHashCode);
}
render(true);
}
}

View File

@@ -0,0 +1,50 @@
package cz.jzitnik.client.game.objects;
import cz.jzitnik.client.annotations.injectors.InjectDependency;
import cz.jzitnik.client.annotations.injectors.InjectState;
import cz.jzitnik.client.events.DroppedItemRerender;
import cz.jzitnik.client.events.InventoryRerender;
import cz.jzitnik.client.game.GameRoom;
import cz.jzitnik.client.game.GameState;
import cz.jzitnik.client.game.items.GameItem;
import cz.jzitnik.common.models.coordinates.RoomCords;
import cz.jzitnik.client.game.utils.Selectable;
import cz.jzitnik.client.utils.events.EventManager;
import lombok.Getter;
import lombok.RequiredArgsConstructor;
import lombok.Setter;
import java.awt.image.BufferedImage;
import java.io.Serializable;
@Getter
@RequiredArgsConstructor
public final class DroppedItem implements Selectable, Serializable {
private final GameRoom room;
private final RoomCords cords;
private final GameItem item;
@Setter
private boolean isSelected = false;
@Override
public BufferedImage getTexture() {
return item.getTexture();
}
@InjectState
private GameState gameState;
@InjectDependency
private EventManager eventManager;
@Override
public void interact() {
if (!gameState.getPlayer().addItem(item)) {
return;
}
gameState.getCurrentRoom().getDroppedItems().remove(this);
eventManager.emitEvent(new InventoryRerender());
eventManager.emitEvent(new DroppedItemRerender(this));
}
}

View File

@@ -0,0 +1,35 @@
package cz.jzitnik.client.game.objects;
import com.fasterxml.jackson.annotation.JsonIgnore;
import com.fasterxml.jackson.annotation.JsonSubTypes;
import com.fasterxml.jackson.annotation.JsonTypeInfo;
import cz.jzitnik.client.game.utils.Renderable;
import cz.jzitnik.common.models.coordinates.RoomCords;
import cz.jzitnik.client.game.utils.Selectable;
import lombok.Getter;
import lombok.RequiredArgsConstructor;
import lombok.Setter;
import java.awt.image.BufferedImage;
@Getter
@RequiredArgsConstructor
@JsonTypeInfo(
use = JsonTypeInfo.Id.NAME,
property = "objectType"
)
@JsonSubTypes({
@JsonSubTypes.Type(value = Chest.class, name = "chest")
})
public sealed abstract class GameObject implements Renderable, Selectable permits Chest {
@JsonIgnore
private final BufferedImage texture;
@JsonIgnore
private final RoomCords cords;
@JsonIgnore
private final boolean selectable;
@JsonIgnore
@Setter
private boolean selected = false;
}

View File

@@ -0,0 +1,5 @@
package cz.jzitnik.client.game.objects;
public interface Interactable {
void interact();
}

View File

@@ -0,0 +1,14 @@
package cz.jzitnik.client.game.objects;
import cz.jzitnik.client.events.MouseAction;
public interface UIClickHandler {
boolean handleClick(MouseAction mouseAction);
default boolean handleMove(MouseAction ignoredMouseAction) {
return false;
}
default boolean handleElse(MouseAction ignoredMouseAction) {
return false;
}
}

View File

@@ -0,0 +1,52 @@
package cz.jzitnik.client.game.setup;
import cz.jzitnik.client.annotations.Dependency;
import cz.jzitnik.client.annotations.injectors.InjectDependency;
import cz.jzitnik.client.annotations.injectors.InjectState;
import cz.jzitnik.client.events.SendSocketMessageEvent;
import cz.jzitnik.client.game.GameRoom;
import cz.jzitnik.client.game.GameState;
import cz.jzitnik.client.game.Player;
import cz.jzitnik.client.game.ResourceManager;
import cz.jzitnik.client.game.setup.scenes.connect.ServerChoose;
import cz.jzitnik.client.utils.DependencyManager;
import cz.jzitnik.client.utils.events.EventManager;
import cz.jzitnik.common.socket.messages.game.creation.CreateGame;
import lombok.extern.slf4j.Slf4j;
import tools.jackson.core.type.TypeReference;
import tools.jackson.databind.ObjectMapper;
import tools.jackson.databind.ObjectReader;
import java.io.IOException;
import java.util.List;
@Slf4j
@Dependency
public class GameSetup {
@InjectState
private GameState gameState;
@InjectDependency
private ResourceManager resourceManager;
@InjectDependency
private ObjectMapper objectMapper;
@InjectDependency
private DependencyManager dependencyManager;
public void setup() throws IOException {
gameState.setScreen(new ServerChoose(dependencyManager));
ObjectReader roomsReader = objectMapper.readerFor(
new TypeReference<List<GameRoom>>() {
}
).with(dependencyManager);
List<GameRoom> rooms = roomsReader.readValue(
resourceManager.getResourceAsStream("setup/rooms.yaml")
);
gameState.setCurrentRoom(rooms.getFirst());
gameState.setAllRooms(rooms);
}
}

View File

@@ -0,0 +1,77 @@
package cz.jzitnik.client.game.setup.scenes;
import com.googlecode.lanterna.input.KeyType;
import cz.jzitnik.client.annotations.injectors.InjectDependency;
import cz.jzitnik.client.annotations.injectors.InjectState;
import cz.jzitnik.client.events.FullRoomDraw;
import cz.jzitnik.client.events.KeyboardPressEvent;
import cz.jzitnik.client.events.MouseAction;
import cz.jzitnik.client.game.GameState;
import cz.jzitnik.client.screens.Screen;
import cz.jzitnik.client.screens.scenes.BasicImageScene;
import cz.jzitnik.client.screens.scenes.Scene;
import cz.jzitnik.client.sound.SoundPlayer;
import cz.jzitnik.client.utils.DependencyManager;
import cz.jzitnik.client.utils.events.EventManager;
public class GameMenuScene extends Scene {
private static class GameMenuAudioScreen extends Screen {
protected final SoundPlayer soundPlayer = new SoundPlayer();
protected boolean play = true;
@Override
public void fullRender() {
// No render here just basic audio playback
new Thread(() -> {
while (play) {
soundPlayer.playSound("audio/menu.ogg", 30, 100);
}
}).start();
}
@Override
public void handleMouseAction(MouseAction event) {
}
@Override
public void handleKeyboardAction(KeyboardPressEvent event) {
}
}
private static final class ImageScene extends BasicImageScene {
@InjectState
private GameState gameState;
@InjectDependency
private EventManager eventManager;
private final GameMenuAudioScreen gameMenuAudioScreen;
public ImageScene(String filePath, GameMenuAudioScreen gameMenuAudioScreen) {
super(filePath);
this.gameMenuAudioScreen = gameMenuAudioScreen;
}
@Override
public void handleKeyboardAction(KeyboardPressEvent event) {
if (event.getKeyStroke().getKeyType() == KeyType.Enter) {
gameMenuAudioScreen.play = false;
gameMenuAudioScreen.soundPlayer.stopCurrentSound();
gameState.setScreen(null);
eventManager.emitEvent(new FullRoomDraw(true));
}
}
}
public GameMenuScene(DependencyManager dependencyManager) {
GameMenuAudioScreen gameMenuScreen = new GameMenuAudioScreen();
ImageScene basicImageScene = new ImageScene("menu.png", gameMenuScreen);
super(new Screen[]{gameMenuScreen, basicImageScene}, new OnEndAction.Repeat());
dependencyManager.inject(this);
dependencyManager.inject(basicImageScene);
}
}

View File

@@ -0,0 +1,18 @@
package cz.jzitnik.client.game.setup.scenes;
import cz.jzitnik.client.screens.Screen;
import cz.jzitnik.client.screens.scenes.Scene;
import cz.jzitnik.client.screens.scenes.VideoSceneWithAudio;
import cz.jzitnik.client.utils.DependencyManager;
public class IntroScene extends Scene {
public IntroScene(DependencyManager dependencyManager) {
super(
new Screen[]{
new VideoSceneWithAudio("video.mp4", "audio.ogg")
},
new OnEndAction.SwitchToScreen(new GameMenuScene(dependencyManager))
);
dependencyManager.inject(this);
}
}

View File

@@ -0,0 +1,483 @@
package cz.jzitnik.client.game.setup.scenes.connect;
import com.googlecode.lanterna.TerminalSize;
import com.googlecode.lanterna.TextColor;
import com.googlecode.lanterna.graphics.TextGraphics;
import com.googlecode.lanterna.input.KeyType;
import com.googlecode.lanterna.screen.TerminalScreen;
import cz.jzitnik.client.annotations.injectors.InjectDependency;
import cz.jzitnik.client.annotations.injectors.InjectState;
import cz.jzitnik.client.events.KeyboardPressEvent;
import cz.jzitnik.client.events.MouseAction;
import cz.jzitnik.client.events.SendSocketMessageEvent;
import cz.jzitnik.client.game.GameState;
import cz.jzitnik.client.screens.Screen;
import cz.jzitnik.client.screens.scenes.Scene;
import cz.jzitnik.client.socket.Client;
import cz.jzitnik.client.sound.SoundPlayer;
import cz.jzitnik.client.states.TerminalState;
import cz.jzitnik.client.ui.Inventory;
import cz.jzitnik.client.ui.pixels.AlphaPixel;
import cz.jzitnik.client.ui.pixels.Empty;
import cz.jzitnik.client.ui.utils.Input;
import cz.jzitnik.client.utils.DependencyManager;
import cz.jzitnik.client.utils.TextRenderer;
import cz.jzitnik.client.utils.events.EventManager;
import cz.jzitnik.common.socket.messages.game.connection.ConnectToAGame;
import cz.jzitnik.common.socket.messages.game.creation.CreateGame;
import jakarta.websocket.DeploymentException;
import cz.jzitnik.client.ui.utils.Button;
import lombok.extern.slf4j.Slf4j;
import java.awt.*;
import java.io.IOException;
@Slf4j
public class ServerChoose extends Scene {
public ServerChoose(DependencyManager dependencyManager) {
GameMenuAudioScreen gameMenuScreen = new GameMenuAudioScreen();
ServerSelector serverSelector = new ServerSelector();
super(new Screen[]{gameMenuScreen, serverSelector}, new OnEndAction.Repeat());
dependencyManager.inject(this);
dependencyManager.inject(serverSelector);
}
private static class GameMenuAudioScreen extends Screen {
protected final SoundPlayer soundPlayer = new SoundPlayer();
protected boolean play = true;
@Override
public void fullRender() {
// No render here just basic audio playback
new Thread(() -> {
while (play) {
soundPlayer.playSound("audio/menu.ogg", 30, 100);
}
}).start();
}
@Override
public void handleMouseAction(MouseAction event) {
}
@Override
public void handleKeyboardAction(KeyboardPressEvent event) {
}
}
private static final class ServerSelector extends Screen {
private final StringBuilder ipBuffer = new StringBuilder();
private boolean connecting = false;
@InjectDependency
private EventManager eventManager;
@InjectDependency
private Client client;
@InjectState
private TerminalState terminalState;
@InjectDependency
private TextRenderer textRenderer;
@InjectState
private GameState gameState;
@InjectDependency
private DependencyManager dependencyManager;
private void renderInput(boolean refresh) {
var tg = terminalState.getTextGraphics();
TerminalScreen screen = terminalState.getTerminalScreen();
Input input = new Input(ipBuffer.toString(), 18, 100);
var inputBuffer = input.render(textRenderer);
TerminalSize termSize = screen.getTerminalSize();
int renderPixelWidth = inputBuffer[0].length;
int renderPixelHeight = inputBuffer.length;
int renderCharWidth = renderPixelWidth;
int renderCharHeight = (renderPixelHeight + 1) / 2;
int startX = (termSize.getColumns() - renderCharWidth) / 2;
int startY = (termSize.getRows() - renderCharHeight) / 2;
for (int y = 0; y < inputBuffer.length; y += 2) {
for (int x = 0; x < inputBuffer[y].length; x++) {
AlphaPixel bottomPixel;
AlphaPixel topPixel = inputBuffer[y][x];
if (y + 1 < inputBuffer.length) {
bottomPixel = inputBuffer[y + 1][x];
} else {
bottomPixel = new Empty();
}
int termX = startX + x;
int termY = startY + (y / 2);
tg.setBackgroundColor(topPixel instanceof Empty ? TextColor.ANSI.BLACK : topPixel.getColor());
tg.setForegroundColor(bottomPixel instanceof Empty ? TextColor.ANSI.BLACK : bottomPixel.getColor());
tg.setCharacter(termX, termY, '▄');
}
}
if (refresh) {
try {
screen.refresh(com.googlecode.lanterna.screen.Screen.RefreshType.DELTA);
} catch (IOException e) {
throw new RuntimeException(e);
}
}
}
@Override
public void fullRender() {
TerminalScreen screen = terminalState.getTerminalScreen();
var tg = terminalState.getTextGraphics();
screen.clear();
TerminalSize terminalSize = screen.getTerminalSize();
for (int y = 0; y < terminalSize.getRows(); y += 1) {
for (int x = 0; x < terminalSize.getColumns(); x++) {
tg.setBackgroundColor(TextColor.ANSI.BLACK);
tg.setForegroundColor(TextColor.ANSI.BLACK);
tg.setCharacter(x, y, '▄');
}
}
AlphaPixel[][] selectServer = textRenderer.renderText("Enter server IP", terminalSize.getColumns(), 20, Color.WHITE, 15f, true);
render(selectServer, 0, 10, tg);
renderInput(false);
try {
screen.refresh(com.googlecode.lanterna.screen.Screen.RefreshType.COMPLETE);
} catch (IOException e) {
throw new RuntimeException(e);
}
}
@Override
public void handleKeyboardAction(KeyboardPressEvent event) {
if (connecting) {
return;
}
if (event.getKeyStroke().getKeyType() == KeyType.Enter) {
try {
connecting = true;
client.connect(ipBuffer.toString());
Screen screen = new ActionSelector();
dependencyManager.inject(screen);
gameState.setScreen(screen);
screen.fullRender();
} catch (DeploymentException | IOException e) {
connecting = false;
}
return;
}
if (event.getKeyStroke().getKeyType() == KeyType.Backspace) {
if (ipBuffer.isEmpty()) {
return;
}
ipBuffer.deleteCharAt(ipBuffer.length() - 1);
renderInput(true);
return;
}
if (event.getKeyStroke().getKeyType() == KeyType.Character && !event.getKeyStroke().isCtrlDown()) {
ipBuffer.append(event.getKeyStroke().getCharacter());
renderInput(true);
}
}
@Override
public void handleMouseAction(MouseAction event) {
}
}
private static final class ActionSelector extends Screen {
@InjectDependency
private TextRenderer textRenderer;
@InjectState
private TerminalState terminalState;
@InjectDependency
private EventManager eventManager;
@InjectState
private GameState gameState;
private int selectedIndex = -1;
@Override
public void fullRender() {
TerminalScreen screen = terminalState.getTerminalScreen();
var tg = terminalState.getTextGraphics();
screen.clear();
TerminalSize terminalSize = screen.getTerminalSize();
for (int y = 0; y < terminalSize.getRows(); y += 1) {
for (int x = 0; x < terminalSize.getColumns(); x++) {
tg.setBackgroundColor(TextColor.ANSI.BLACK);
tg.setForegroundColor(TextColor.ANSI.BLACK);
tg.setCharacter(x, y, '▄');
}
}
AlphaPixel[][] selectAction = textRenderer.renderText("Select action", terminalSize.getColumns(), 20, Color.WHITE, 15f, true);
render(selectAction, 0, 10, tg);
renderButtons();
try {
screen.refresh(com.googlecode.lanterna.screen.Screen.RefreshType.COMPLETE);
} catch (IOException e) {
throw new RuntimeException(e);
}
}
private static final int BUTTON_HEIGHT = 20;
private static final int BUTTON_WIDTH = 200;
private static final int BUTTON_GAP = 10;
private static final int BUTTON_COUNT = 2;
private static final int BUTTONS_HEIGHT = BUTTON_HEIGHT * BUTTON_COUNT + (BUTTON_COUNT - 1) * BUTTON_GAP;
private void renderButtons() {
var tg = terminalState.getTextGraphics();
TerminalSize terminalSize = terminalState.getTerminalScreen().getTerminalSize();
final int BUTTON_PAD_X = terminalSize.getColumns() / 2 - BUTTON_WIDTH / 2;
final int BUTTON_PAD_Y = terminalSize.getRows() - BUTTONS_HEIGHT / 2;
Button button = new Button(
Inventory.BORDER_COLOR,
Inventory.BACKGROUND_COLOR,
Inventory.BACKGROUND_COLOR_HOVERED,
Color.WHITE,
BUTTON_HEIGHT,
BUTTON_WIDTH,
1,
15f,
textRenderer
);
render(button.render("Create a world", selectedIndex == 0), BUTTON_PAD_X, BUTTON_PAD_Y, tg);
render(button.render("Connect to an existing world", selectedIndex == 1), BUTTON_PAD_X, BUTTON_PAD_Y + (BUTTON_HEIGHT + BUTTON_GAP), tg);
}
@Override
public void handleMouseAction(MouseAction event) {
TerminalSize terminalSize = terminalState.getTerminalScreen().getTerminalSize();
final int BUTTON_START_X = terminalSize.getColumns() / 2 - BUTTON_WIDTH / 2;
final int BUTTON_START_Y = terminalSize.getRows() - BUTTONS_HEIGHT / 2;
final int BUTTON_END_X = BUTTON_START_X + BUTTON_WIDTH;
final int BUTTON_END_Y = BUTTON_START_Y + BUTTONS_HEIGHT;
final int TERMINAL_X = event.getPosition().getColumn();
final int TERMINAL_Y = event.getPosition().getRow() * 2;
final int SINGLE_BUTTON_HEIGHT = BUTTON_HEIGHT + BUTTON_GAP;
int index = (TERMINAL_Y - BUTTON_START_Y) / SINGLE_BUTTON_HEIGHT;
int rest = (TERMINAL_Y - BUTTON_START_Y) % SINGLE_BUTTON_HEIGHT;
if (!(TERMINAL_X >= BUTTON_START_X && TERMINAL_Y >= BUTTON_START_Y && TERMINAL_X < BUTTON_END_X && TERMINAL_Y < BUTTON_END_Y) || rest > BUTTON_HEIGHT) {
if (selectedIndex != -1) {
selectedIndex = -1;
renderButtons();
refresh();
}
return;
}
switch (event.getActionType()) {
case MOVE -> {
selectedIndex = index;
renderButtons();
refresh();
}
case CLICK_RELEASE -> {
switch (index) {
case 0 -> eventManager.emitEvent(new SendSocketMessageEvent(new CreateGame()));
case 1 -> {
Screen screen = new ConnectWorld();
gameState.setScreen(screen);
screen.fullRender();
}
}
}
}
}
private void refresh() {
try {
terminalState.getTerminalScreen().refresh(com.googlecode.lanterna.screen.Screen.RefreshType.DELTA);
} catch (IOException e) {
throw new RuntimeException(e);
}
}
@Override
public void handleKeyboardAction(KeyboardPressEvent event) {
}
}
private static final class ConnectWorld extends Screen {
private final StringBuilder passBuffer = new StringBuilder();
private boolean connecting = false;
@InjectDependency
private EventManager eventManager;
@InjectDependency
private Client client;
@InjectState
private TerminalState terminalState;
@InjectDependency
private TextRenderer textRenderer;
@InjectState
private GameState gameState;
@InjectDependency
private DependencyManager dependencyManager;
private void renderInput(boolean refresh) {
var tg = terminalState.getTextGraphics();
TerminalScreen screen = terminalState.getTerminalScreen();
Input input = new Input(passBuffer.toString(), 18, 100);
var inputBuffer = input.render(textRenderer);
TerminalSize termSize = screen.getTerminalSize();
int renderPixelWidth = inputBuffer[0].length;
int renderPixelHeight = inputBuffer.length;
int renderCharWidth = renderPixelWidth;
int renderCharHeight = (renderPixelHeight + 1) / 2;
int startX = (termSize.getColumns() - renderCharWidth) / 2;
int startY = (termSize.getRows() - renderCharHeight) / 2;
for (int y = 0; y < inputBuffer.length; y += 2) {
for (int x = 0; x < inputBuffer[y].length; x++) {
AlphaPixel bottomPixel;
AlphaPixel topPixel = inputBuffer[y][x];
if (y + 1 < inputBuffer.length) {
bottomPixel = inputBuffer[y + 1][x];
} else {
bottomPixel = new Empty();
}
int termX = startX + x;
int termY = startY + (y / 2);
tg.setBackgroundColor(topPixel instanceof Empty ? TextColor.ANSI.BLACK : topPixel.getColor());
tg.setForegroundColor(bottomPixel instanceof Empty ? TextColor.ANSI.BLACK : bottomPixel.getColor());
tg.setCharacter(termX, termY, '▄');
}
}
if (refresh) {
try {
screen.refresh(com.googlecode.lanterna.screen.Screen.RefreshType.DELTA);
} catch (IOException e) {
throw new RuntimeException(e);
}
}
}
@Override
public void fullRender() {
TerminalScreen screen = terminalState.getTerminalScreen();
var tg = terminalState.getTextGraphics();
screen.clear();
TerminalSize terminalSize = screen.getTerminalSize();
for (int y = 0; y < terminalSize.getRows(); y += 1) {
for (int x = 0; x < terminalSize.getColumns(); x++) {
tg.setBackgroundColor(TextColor.ANSI.BLACK);
tg.setForegroundColor(TextColor.ANSI.BLACK);
tg.setCharacter(x, y, '▄');
}
}
AlphaPixel[][] selectServer = textRenderer.renderText("Enter world password", terminalSize.getColumns(), 20, Color.WHITE, 15f, true);
render(selectServer, 0, 10, tg);
renderInput(false);
try {
screen.refresh(com.googlecode.lanterna.screen.Screen.RefreshType.COMPLETE);
} catch (IOException e) {
throw new RuntimeException(e);
}
}
@Override
public void handleKeyboardAction(KeyboardPressEvent event) {
if (connecting) {
return;
}
if (event.getKeyStroke().getKeyType() == KeyType.Enter) {
connecting = true;
String pass = passBuffer.toString();
eventManager.emitEvent(new SendSocketMessageEvent(new ConnectToAGame(pass)));
return;
}
if (event.getKeyStroke().getKeyType() == KeyType.Backspace) {
if (passBuffer.isEmpty()) {
return;
}
passBuffer.deleteCharAt(passBuffer.length() - 1);
renderInput(true);
return;
}
if (event.getKeyStroke().getKeyType() == KeyType.Character && !event.getKeyStroke().isCtrlDown()) {
passBuffer.append(event.getKeyStroke().getCharacter());
renderInput(true);
}
}
@Override
public void handleMouseAction(MouseAction event) {
}
}
private static void render(AlphaPixel[][] buffer, int padX, int padY, TextGraphics tg) {
for (int y = 0; y < buffer.length; y += 2) {
for (int x = 0; x < buffer[y].length; x++) {
AlphaPixel topPixel = buffer[y][x];
AlphaPixel bottomPixel;
if (y + 1 < buffer.length) {
bottomPixel = buffer[y + 1][x];
} else {
bottomPixel = new Empty();
}
int termX = padX + x;
int termY = padY / 2 + y / 2;
tg.setBackgroundColor(topPixel instanceof Empty ? TextColor.ANSI.BLACK : topPixel.getColor());
tg.setForegroundColor(bottomPixel instanceof Empty ? TextColor.ANSI.BLACK : bottomPixel.getColor());
tg.setCharacter(termX, termY, '▄');
}
}
}
}

Some files were not shown because too many files have changed in this diff Show More