feat: A lot of new features

This commit is contained in:
2024-12-16 22:19:24 +01:00
parent 7fac24d073
commit acb0cf2164
51 changed files with 1291 additions and 44 deletions

38
frontend/.gitignore vendored Normal file
View File

@ -0,0 +1,38 @@
target/
!.mvn/wrapper/maven-wrapper.jar
!**/src/main/**/target/
!**/src/test/**/target/
### IntelliJ IDEA ###
.idea/modules.xml
.idea/jarRepositories.xml
.idea/compiler.xml
.idea/libraries/
*.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

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

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

7
frontend/.idea/encodings.xml generated Normal file
View File

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

14
frontend/.idea/misc.xml generated Normal file
View File

@ -0,0 +1,14 @@
<?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_23" default="true" project-jdk-name="openjdk-23" project-jdk-type="JavaSDK">
<output url="file://$PROJECT_DIR$/out" />
</component>
</project>

6
frontend/.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>

51
frontend/pom.xml Normal file
View File

@ -0,0 +1,51 @@
<?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>
<groupId>cz.jzitnik</groupId>
<artifactId>chronos</artifactId>
<version>1.0-SNAPSHOT</version>
<properties>
<maven.compiler.source>23</maven.compiler.source>
<maven.compiler.target>23</maven.compiler.target>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<java.version>23</java.version>
</properties>
<dependencies>
<dependency>
<groupId>com.squareup.retrofit2</groupId>
<artifactId>retrofit</artifactId>
<version>2.9.0</version>
</dependency>
<dependency>
<groupId>com.squareup.retrofit2</groupId>
<artifactId>converter-jackson</artifactId>
<version>2.9.0</version>
</dependency>
<dependency>
<groupId>org.apache.httpcomponents.client5</groupId>
<artifactId>httpclient5</artifactId>
<version>5.2</version>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
<version>1.18.36</version>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
<version>2.15.2</version> <!-- Use the latest version -->
</dependency>
</dependencies>
</project>

View File

@ -0,0 +1,56 @@
package cz.jzitnik;
import cz.jzitnik.game.Chronos;
import cz.jzitnik.utils.Cli;
import java.io.IOException;
public class Main {
public static void main(String[] args) throws IOException {
Chronos chronos = new Chronos();
Cli.printHeader("Načítání hry Chronos");
Cli.info("Načítání konfigurace...");
if (chronos.hasConfig()) {
chronos.loadData();
Cli.success("Konfigurace byla úspěšně nalezena a načtena!");
} else {
// Setup a server
Cli.info("Konfigurace nebyla nalezena! Vytvářím novou.");
chronos.setup();
}
if (!chronos.hasUser()) {
System.out.println();
Cli.type("Nyní si buď vytvoříte vlastní hru a nebo se připojíte do již existující hry");
System.out.println();
String[] options = {"Vytvořit novou hru", "Připojit se do již existující hry"};
var option = Cli.selectOption(options);
switch (option) {
case "Vytvořit novou hru":
chronos.createNewGame();
chronos.addUser();
Cli.success("Byl jste úspěšně zaregistrován jako první hráč!");
chronos.adminPanel();
break;
case "Připojit se do hry":
chronos.connectToExisting();
chronos.addUser();
Cli.success("Byl jste úspěšně zaregistrován jako hráč!");
break;
default:
System.out.println("Error"); // This will never happen
}
}
chronos.waitForStart();
Cli.info("Hra začala!");
System.out.println("\n\n");
Cli.printHeader("Chronos");
chronos.visit();
}
}

View File

@ -0,0 +1,55 @@
package cz.jzitnik.api;
import cz.jzitnik.api.requests.PlayerNameRequest;
import cz.jzitnik.api.responses.UnifiedResponse;
import cz.jzitnik.api.types.Game;
import cz.jzitnik.api.types.Room;
import cz.jzitnik.api.types.TakeItemsResponse;
import cz.jzitnik.api.types.TestGameKeyResponse;
import retrofit2.Call;
import retrofit2.http.Body;
import retrofit2.http.GET;
import retrofit2.http.POST;
import retrofit2.http.Query;
public interface ApiService {
@GET("status")
Call<UnifiedResponse<String, Error>> status();
@POST("game/new")
Call<UnifiedResponse<Game, Error>> newGame();
@POST("game/players/new")
Call<UnifiedResponse<String, Error>> newPlayer(
@Query("gameKey") String gameKey,
@Body PlayerNameRequest requestBody
);
@POST("game/start")
Call<UnifiedResponse<Object, Error>> startGame(
@Query("playerKey") String playerKey,
@Query("gameKey") String gameKey
);
@GET("game/test")
Call<UnifiedResponse<TestGameKeyResponse, Error>> testGameKey(
@Query("gameKey") String gameKey
);
@GET("game/rooms/current_room")
Call<UnifiedResponse<Room, Error>> getCurrentRoom(
@Query("playerKey") String playerKey
);
@POST("game/rooms/move")
Call<UnifiedResponse<Object, Error>> moveToRoom(
@Query("playerKey") String playerKey,
@Query("roomId") Long roomId
);
@POST("game/characters/take_items")
Call<UnifiedResponse<TakeItemsResponse, Error>> takeItems(
@Query("playerKey") String playerKey,
@Query("characterId") Long characterId
);
}

View File

@ -0,0 +1,12 @@
package cz.jzitnik.api.requests;
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.Setter;
@Getter
@Setter
@AllArgsConstructor
public class PlayerNameRequest {
private String name;
}

View File

@ -0,0 +1,20 @@
package cz.jzitnik.api.responses;
import lombok.Getter;
import java.util.Optional;
@Getter
public class UnifiedResponse<T, U> {
private Optional<T> data;
private Optional<U> error;
private Boolean success;
public void setData(T data) {
this.data = Optional.ofNullable(data);
}
public void setError(U error) {
this.error = Optional.ofNullable(error);
}
}

View File

@ -0,0 +1,24 @@
package cz.jzitnik.api.types;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
import java.util.List;
@Getter
@Setter
@NoArgsConstructor
public class Character {
private Long id;
private String name;
private List<Item> inventory;
private Interaction interaction;
private boolean interactedWith;
private String dialog;
}

View File

@ -0,0 +1,24 @@
package cz.jzitnik.api.types;
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
import java.util.List;
@Getter
@Setter
@NoArgsConstructor
@AllArgsConstructor
public class Game {
private Long id;
private String gameKey;
private List<Player> players;
private Room defaultRoom;
private boolean started;
}

View File

@ -0,0 +1,5 @@
package cz.jzitnik.api.types;
public enum Interaction {
Farmer
}

View File

@ -0,0 +1,12 @@
package cz.jzitnik.api.types;
import lombok.Getter;
import lombok.Setter;
@Getter
@Setter
public class Item {
private Long id;
private ItemType itemType;
}

View File

@ -0,0 +1,6 @@
package cz.jzitnik.api.types;
public enum ItemType {
KEY_FRAGMENT,
LUCK_POTION
}

View File

@ -0,0 +1,18 @@
package cz.jzitnik.api.types;
import lombok.Getter;
import lombok.Setter;
import java.util.List;
@Getter
@Setter
public class Player {
private Long id;
private String name;
private List<Item> inventory;
private Room currentRoom;
}

View File

@ -0,0 +1,18 @@
package cz.jzitnik.api.types;
import lombok.Getter;
import lombok.Setter;
import java.util.List;
@Getter
@Setter
public class Room {
private Long id;
private String name;
private List<Item> items;
private List<Character> characters;
}

View File

@ -0,0 +1,6 @@
package cz.jzitnik.api.types;
public enum TakeItemsResponse {
DONE,
ALREADY_TAKEN
}

View File

@ -0,0 +1,8 @@
package cz.jzitnik.api.types;
public enum TestGameKeyResponse {
WORKING,
GAME_STARTED,
INVALID_KEY,
MAXIUM_PLAYERS
}

View File

@ -0,0 +1,243 @@
package cz.jzitnik.game;
import cz.jzitnik.api.ApiService;
import cz.jzitnik.api.requests.PlayerNameRequest;
import cz.jzitnik.api.types.Character;
import cz.jzitnik.api.types.Room;
import cz.jzitnik.api.types.TakeItemsResponse;
import cz.jzitnik.api.types.TestGameKeyResponse;
import cz.jzitnik.utils.Cli;
import cz.jzitnik.utils.ConfigPathProvider;
import lombok.Getter;
import lombok.NoArgsConstructor;
import java.io.Console;
import java.io.File;
import java.io.IOException;
import java.net.ConnectException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.Arrays;
import java.util.Objects;
import com.fasterxml.jackson.databind.ObjectMapper;
import retrofit2.Retrofit;
import retrofit2.converter.jackson.JacksonConverterFactory;
import retrofit2.http.GET;
@NoArgsConstructor
@Getter
public class Chronos {
private static final String IPV4_PATTERN =
"^(([01]?\\d\\d?|2[0-4]\\d|25[0-5])\\.){3}([01]?\\d\\d?|2[0-4]\\d|25[0-5])$";
private static boolean isValidIPv4(String ip) {
return ip.matches(IPV4_PATTERN);
}
private LocalData localData = new LocalData();
private ApiService apiService;
public boolean hasConfig() {
var config_path = ConfigPathProvider.getPath();
File file = new File(Path.of(config_path, "config.json").toString());
return file.exists();
}
public boolean hasUser() {
return localData.getUserSecret() != null;
}
public void loadData() throws IOException {
var config_path = ConfigPathProvider.getPath();
File file = new File(Path.of(config_path, "config.json").toString());
String content = Files.readString(file.toPath());
ObjectMapper objectMapper = new ObjectMapper();
try {
this.localData = objectMapper.readValue(content, LocalData.class);
} catch (Exception e) {
e.printStackTrace();
}
setupApi(localData.getServer());
}
private void setupApi(String baseUrl) {
Retrofit retrofit = new Retrofit.Builder()
.baseUrl(baseUrl)
.addConverterFactory(JacksonConverterFactory.create(new ObjectMapper()))
.build();
this.apiService = retrofit.create(ApiService.class);
}
public void setup() throws IOException {
System.out.println("\n");
Cli.type("Nyní se budeme muset připojit na Chronos server. Zadejte prosím IP adresu serveru na kterém běží chronos server.\nNapř: 127.0.0.1");
Console console = System.console();
String ipaddr = null;
do {
ipaddr = console.readLine(ipaddr == null ? "Zadejte ip adresu serveru: " : "Zadejte platnou ip adresu serveru: ");
} while (!isValidIPv4(ipaddr));
Cli.info("Pokouším se připojit k serveru");
String baseUrl = "http://" + ipaddr + ":8080/";
setupApi(baseUrl);
try {
var response = apiService.status().execute().body();
if (response.getData().isEmpty() || !response.getData().get().equals("working")) {
Cli.error("Tento server pravděpodobně není Chronos server.");
setup();
return;
}
} catch (ConnectException e) {
Cli.error("Nastala chyba při připojování k serveru, zkuste to znovu");
setup();
return;
}
Cli.success("Server byl úspěšně nastaven");
this.localData.setServer(baseUrl);
this.localData.saveData();
}
public void createNewGame() throws IOException {
Cli.printHeader("Vytvoření hry");
Cli.info("Vytvářím novou hru");
var body = apiService.newGame().execute().body();
if (!Objects.requireNonNull(body).getSuccess()) {
System.err.println("Nastala chyba při vytváření hry!");
System.exit(1);
}
var data = body.getData().get();
localData.setGameKey(data.getGameKey());
Cli.success("Byla úspěšně vytvořena hra");
}
public void addUser() throws IOException {
Cli.printHeader("Vytvoření hráče");
Console console = System.console();
var name = console.readLine("Zadejte vaše jméno: ");
var newPlayerBody = apiService.newPlayer(localData.getGameKey(), new PlayerNameRequest(name)).execute().body();
if (!Objects.requireNonNull(newPlayerBody).getSuccess()) {
System.err.println("Nastala chyba při vytváření hráče!");
System.exit(1);
}
localData.setUserSecret(newPlayerBody.getData().get());
}
public void adminPanel() throws IOException {
Cli.printHeader("Čekání na připojení hráčů");
System.out.println();
Cli.center("Kód pro připojení:");
System.out.print(Cli.Colors.GREEN);
Cli.center(localData.getGameKey());
System.out.print(Cli.Colors.RESET);
Cli.center("(pomocí tohoto kódu se hráči můžou napojit)");
System.out.println("\nPo startu hry už se nemůžou napojit noví hráči!");
System.out.println("Stiskněte " + Cli.Colors.CYAN + "Enter" + Cli.Colors.RESET + " pro start hry...");
System.console().readLine();
localData.saveData();
apiService.startGame(localData.getUserSecret(), localData.getGameKey()).execute();
}
public void connectToExisting() throws IOException {
Console console = System.console();
Cli.printHeader("Připojení do hry");
System.out.println();
String code = console.readLine("Zadejte kod pro připojení: ");
var testGameKeyResponse = apiService.testGameKey(code).execute().body().getData().get();
if (testGameKeyResponse == TestGameKeyResponse.INVALID_KEY) {
Cli.error("Nebyla nalezena hra s tímto kódem!");
connectToExisting();
return;
}
if (testGameKeyResponse == TestGameKeyResponse.GAME_STARTED) {
Cli.error("Tato hra již začala!");
connectToExisting();
return;
}
if (testGameKeyResponse == TestGameKeyResponse.MAXIUM_PLAYERS) {
Cli.error("V této hře je již maximální počet hráčů!");
connectToExisting();
return;
}
localData.setGameKey(code);
}
public void waitForStart() throws IOException {
boolean started = false;
while (!started) {
var res = apiService.testGameKey(localData.getGameKey()).execute();
var data = res.body().getData().get();
started = data == TestGameKeyResponse.GAME_STARTED;
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
}
public void visit() throws IOException {
var res = apiService.getCurrentRoom(localData.getUserSecret()).execute();
var room = res.body().getData().get();
visit(room, false);
}
public void visit(Room room) throws IOException {
visit(room, true);
}
public void visit(Room room, boolean changeRoom) throws IOException {
if (changeRoom) {
apiService.moveToRoom(localData.getUserSecret(), room.getId());
}
Cli.info("Nyní se nacházíte v místnosti " + room.getName());
if (room.getCharacters().isEmpty()) {
Cli.type("V místnosti je prázno... Ani jedna duše.");
}
for (Character character : room.getCharacters()) {
talkWith(character);
}
}
public void talkWith(Character character) throws IOException {
Cli.type("V místnosti je " + character.getName());
System.out.println();
Cli.type(Cli.Colors.YELLOW + character.getName() + ": " + Cli.Colors.RESET + character.getDialog());
if (!character.getInventory().isEmpty()) {
var res = apiService.takeItems(getLocalData().getUserSecret(), character.getId()).execute();
var body = res.body().getData().get();
if (body == TakeItemsResponse.ALREADY_TAKEN) {
Cli.type(Cli.Colors.YELLOW + character.getName() + ": " + Cli.Colors.RESET + "Už jsem ti mé itemy dal. Už pro tebe nic nemám.");
} else {
var characters = character.getInventory().stream().map(item -> item.getItemType().toString()).toList();
Cli.info("Dostal jste: " + String.join(", ", characters));
}
}
}
}

View File

@ -0,0 +1,41 @@
package cz.jzitnik.game;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import cz.jzitnik.utils.ConfigPathProvider;
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
import java.io.File;
import java.io.IOException;
import java.nio.file.Path;
@Getter
@Setter
@NoArgsConstructor
public class LocalData {
private String server;
private String userSecret;
private String gameKey;
public void saveData() {
ObjectMapper objectMapper = new ObjectMapper();
try {
String configPath = ConfigPathProvider.getPath();
File file = new File(Path.of(configPath, "config.json").toString());
File parentDir = file.getParentFile();
if (!parentDir.exists()) {
parentDir.mkdirs();
}
objectMapper.writeValue(file, this);
} catch (JsonProcessingException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
}
}
}

View File

@ -0,0 +1,172 @@
package cz.jzitnik.utils;
import java.util.Scanner;
public class Cli {
public static class Colors {
public static final String RESET = "\033[0m"; // ANSI escape code to reset the terminal
// Text colors
public static final String BLACK = "\033[0;30m";
public static final String RED = "\033[0;31m";
public static final String GREEN = "\033[0;32m";
public static final String YELLOW = "\033[0;33m";
public static final String BLUE = "\033[0;34m";
public static final String PURPLE = "\033[0;35m";
public static final String CYAN = "\033[0;36m";
public static final String WHITE = "\033[0;37m";
// Bold text colors (in bold)
public static final String BOLD_BLACK = "\033[1;30m";
public static final String BOLD_RED = "\033[1;31m";
public static final String BOLD_GREEN = "\033[1;32m";
public static final String BOLD_YELLOW = "\033[1;33m";
public static final String BOLD_BLUE = "\033[1;34m";
public static final String BOLD_PURPLE = "\033[1;35m";
public static final String BOLD_CYAN = "\033[1;36m";
public static final String BOLD_WHITE = "\033[1;37m";
// Background colors
public static final String BLACK_BACKGROUND = "\033[40m";
public static final String RED_BACKGROUND = "\033[41m";
public static final String GREEN_BACKGROUND = "\033[42m";
public static final String YELLOW_BACKGROUND = "\033[43m";
public static final String BLUE_BACKGROUND = "\033[44m";
public static final String PURPLE_BACKGROUND = "\033[45m";
public static final String CYAN_BACKGROUND = "\033[46m";
public static final String WHITE_BACKGROUND = "\033[47m";
// Bold background colors
public static final String BOLD_BLACK_BACKGROUND = "\033[1;40m";
public static final String BOLD_RED_BACKGROUND = "\033[1;41m";
public static final String BOLD_GREEN_BACKGROUND = "\033[1;42m";
public static final String BOLD_YELLOW_BACKGROUND = "\033[1;43m";
public static final String BOLD_BLUE_BACKGROUND = "\033[1;44m";
public static final String BOLD_PURPLE_BACKGROUND = "\033[1;45m";
public static final String BOLD_CYAN_BACKGROUND = "\033[1;46m";
public static final String BOLD_WHITE_BACKGROUND = "\033[1;47m";
}
public static void printHeader(String text) {
int totalWidth = 100;
int padding = (totalWidth - text.length()) / 2;
StringBuilder sb = new StringBuilder(totalWidth);
for (int i = 0; i < padding; i++) {
sb.append("=");
}
sb.append(" ");
sb.append(text);
sb.append(" ");
for (int i = 0; i < padding; i++) {
sb.append("=");
}
while (sb.length() < totalWidth) {
sb.insert(0, "=");
}
System.out.println();
System.out.println(sb);
}
public static void center(String text) {
int totalWidth = 100;
int padding = (totalWidth - text.length()) / 2;
StringBuilder sb = new StringBuilder(totalWidth);
for (int i = 0; i < padding; i++) {
sb.append(" ");
}
sb.append(text);
for (int i = 0; i < padding; i++) {
sb.append(" ");
}
while (sb.length() < totalWidth) {
sb.insert(0, " ");
}
System.out.println(sb);
}
public static String wrapText(String text) {
return wrapText(text, 0);
}
public static String wrapText(String text, int charsRemove) {
StringBuilder wrappedText = new StringBuilder();
int line = 0;
var words = text.split(" ");
for (var word : words) {
if ((line + word.length() + 1) > (100 - charsRemove)) {
wrappedText.append("\n");
line = 0;
}
line += word.length() + 1;
wrappedText.append(word).append(" ");
}
return wrappedText.toString();
}
public static void typeText(String text) {
for (int i = 0; i < text.length(); i++) {
System.out.print(text.charAt(i));
try {
Thread.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println();
}
public static void type(String text) {
Cli.typeText(Cli.wrapText(text));
}
public static void info(String text) {
System.out.println(Colors.BLUE + "Info: " + Colors.RESET + Cli.wrapText(text, 6));
}
public static void success(String text) {
System.out.println(Colors.GREEN + "Úspěch: " + Colors.RESET + Cli.wrapText(text, 8));
}
public static void error(String text) {
System.out.println(Colors.RED + "Chyba: " + Colors.RESET + Cli.wrapText(text, 7));
}
/**
* Displays a menu with the provided options and returns the selected option.
*
* @param options Array of options to display to the user.
* @return The selected option as a String.
*/
public static String selectOption(String[] options) {
Scanner scanner = new Scanner(System.in);
int choice;
System.out.println("Vyberte možnost:");
for (int i = 0; i < options.length; i++) {
System.out.println((i + 1) + ". " + options[i]);
}
while (true) {
System.out.print("Vložte číslo možnosti: ");
if (scanner.hasNextInt()) {
choice = scanner.nextInt();
if (choice >= 1 && choice <= options.length) {
return options[choice - 1];
} else {
System.out.println("Neplatní možnost, vyberte číslo mezi 1 a " + options.length);
}
} else {
System.out.println("Neplatný vstup, vložte číslo.");
scanner.next();
}
}
}
}

View File

@ -0,0 +1,19 @@
package cz.jzitnik.utils;
public class ConfigPathProvider {
public static String getPath() {
String configDir;
String os = System.getProperty("os.name").toLowerCase();
if (os.contains("win")) {
configDir = System.getenv("APPDATA") + "\\Chronos";
} else if (os.contains("nix") || os.contains("nux")) {
configDir = System.getProperty("user.home") + "/.config/Chronos";
} else if (os.contains("mac")) {
configDir = System.getProperty("user.home") + "/Library/Application Support/Chronos";
} else {
configDir = System.getProperty("user.home") + "/Chronos";
}
return configDir;
}
}