diff --git a/README.md b/README.md index e12151d..88e3385 100644 --- a/README.md +++ b/README.md @@ -6,7 +6,7 @@ Jednoduchá multiplayer hra napsaná v Javě. Backend je napsaný v Javě pomocí Spring. Jedná se o jednoduchý HTTP server. -**Ano jsem debil, využít relační databázi nebyl rozhodě dobrý nápad. Došlo mi to uprostřed programování a už se mi nechtělo vracet zpět.** +**Ano jsem debil, využít relační databázi nebyl rozhodně dobrý nápad. Došlo mi to uprostřed programování a už se mi nechtělo vracet zpět.** ### Enviromental variables diff --git a/backend/src/main/java/cz/jzitnik/chronos/controllers/PlayerController.java b/backend/src/main/java/cz/jzitnik/chronos/controllers/PlayerController.java index a31a10e..8c3b842 100644 --- a/backend/src/main/java/cz/jzitnik/chronos/controllers/PlayerController.java +++ b/backend/src/main/java/cz/jzitnik/chronos/controllers/PlayerController.java @@ -1,13 +1,17 @@ package cz.jzitnik.chronos.controllers; +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.ObjectMapper; import cz.jzitnik.chronos.entities.Item; import cz.jzitnik.chronos.entities.Message; import cz.jzitnik.chronos.entities.MessageType; import cz.jzitnik.chronos.entities.Player; +import cz.jzitnik.chronos.payload.errors.FullInventoryError; import cz.jzitnik.chronos.payload.errors.ItemNotUsableException; import cz.jzitnik.chronos.payload.errors.NotFoundError; import cz.jzitnik.chronos.payload.requests.PlayerNameRequest; import cz.jzitnik.chronos.payload.responses.InventoryFullResponse; +import cz.jzitnik.chronos.payload.responses.ItemMoveMessageResponse; import cz.jzitnik.chronos.payload.responses.UnifiedResponse; import cz.jzitnik.chronos.repository.GameRepository; import cz.jzitnik.chronos.repository.ItemRepository; @@ -19,6 +23,8 @@ import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.*; +import java.util.List; + @CrossOrigin(origins = "*", maxAge = 3600) @RestController @RequestMapping("/game/players") @@ -116,4 +122,58 @@ public class PlayerController { return ResponseEntity.ok(UnifiedResponse.success(item)); } + + @GetMapping + @CheckUser + public ResponseEntity, Error>> getAllPlayers(@RequestParam String playerKey) { + var player = playerRepository.findByPlayerKey(playerKey).get(); + var game = player.getGame(); + + var players = game.getPlayers(); + + return ResponseEntity.ok(UnifiedResponse.success(players)); + } + + @PostMapping("/send_item") + @CheckUser + public ResponseEntity> sendItem(@RequestParam String playerKey, @RequestParam Long itemId, @RequestParam Long playerId) { + var player = playerRepository.findByPlayerKey(playerKey).get(); + + var player2Optional = playerRepository.findById(playerId); + if (player2Optional.isEmpty()) { + return ResponseEntity.status(HttpStatus.NOT_FOUND).body(UnifiedResponse.failure(new NotFoundError("Player with id " + playerId + " was not found!"))); + } + var player2 = player2Optional.get(); + + var itemOptional = itemRepository.findById(itemId); + if (itemOptional.isEmpty()) { + return ResponseEntity.status(HttpStatus.NOT_FOUND).body(UnifiedResponse.failure(new NotFoundError("Item with id " + itemId + " was not found!"))); + } + var item = itemOptional.get(); + + if (item.getOwner() == null || !item.getOwner().getId().equals(player.getId())) { + return ResponseEntity.status(HttpStatus.FORBIDDEN).body(UnifiedResponse.failure(new Error("You don't own this item!"))); + } + if (player.getInventorySize() - player.getInventory().size() == 0) { + // Inventory is full + return ResponseEntity.status(HttpStatus.CONFLICT).body(UnifiedResponse.failure(new FullInventoryError("Player has full inventory!"))); + } + + item.setOwner(player2); + itemRepository.save(item); + + ObjectMapper objectMapper = new ObjectMapper(); + try { + var json = objectMapper.writeValueAsString(new ItemMoveMessageResponse(item.getItemType(), player2.getName())); + + var message = new Message(player, json, MessageType.ITEM_MOVED); + + player.getMessages().add(message); + playerRepository.save(player); + + return ResponseEntity.ok(UnifiedResponse.success(null)); + } catch (JsonProcessingException e) { + throw new RuntimeException(e); + } + } } diff --git a/backend/src/main/java/cz/jzitnik/chronos/entities/MessageType.java b/backend/src/main/java/cz/jzitnik/chronos/entities/MessageType.java index e43ecd6..039dd7c 100644 --- a/backend/src/main/java/cz/jzitnik/chronos/entities/MessageType.java +++ b/backend/src/main/java/cz/jzitnik/chronos/entities/MessageType.java @@ -7,5 +7,6 @@ public enum MessageType { LOST_ITEM, ITEM_USED, KEY_FRAGMENT_HANDED_OVER, + ITEM_MOVED, JOINED, } diff --git a/backend/src/main/java/cz/jzitnik/chronos/payload/errors/FullInventoryError.java b/backend/src/main/java/cz/jzitnik/chronos/payload/errors/FullInventoryError.java new file mode 100644 index 0000000..07e1603 --- /dev/null +++ b/backend/src/main/java/cz/jzitnik/chronos/payload/errors/FullInventoryError.java @@ -0,0 +1,7 @@ +package cz.jzitnik.chronos.payload.errors; + +public class FullInventoryError extends Error { + public FullInventoryError(String message) { + super(message); + } +} diff --git a/backend/src/main/java/cz/jzitnik/chronos/payload/responses/ItemMoveMessageResponse.java b/backend/src/main/java/cz/jzitnik/chronos/payload/responses/ItemMoveMessageResponse.java new file mode 100644 index 0000000..2ca9da8 --- /dev/null +++ b/backend/src/main/java/cz/jzitnik/chronos/payload/responses/ItemMoveMessageResponse.java @@ -0,0 +1,14 @@ +package cz.jzitnik.chronos.payload.responses; + +import cz.jzitnik.chronos.entities.ItemType; +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@NoArgsConstructor +@AllArgsConstructor +@Getter +public class ItemMoveMessageResponse { + private ItemType itemType; + private String playerToName; +} diff --git a/frontend/src/main/java/cz/jzitnik/api/ApiService.java b/frontend/src/main/java/cz/jzitnik/api/ApiService.java index 27d846a..cb96c6a 100644 --- a/frontend/src/main/java/cz/jzitnik/api/ApiService.java +++ b/frontend/src/main/java/cz/jzitnik/api/ApiService.java @@ -138,4 +138,16 @@ public interface ApiService { Call> gameWon( @Query("playerKey") String playerKey ); + + @GET("game/players") + Call, Error>> getAllPlayers( + @Query("playerKey") String playerKey + ); + + @POST("game/players/send_item") + Call> sendItem( + @Query("playerKey") String playerKey, + @Query("itemId") Long itemId, + @Query("playerId") Long playerId + ); } diff --git a/frontend/src/main/java/cz/jzitnik/api/responses/ItemMoveMessageResponse.java b/frontend/src/main/java/cz/jzitnik/api/responses/ItemMoveMessageResponse.java new file mode 100644 index 0000000..9d8df08 --- /dev/null +++ b/frontend/src/main/java/cz/jzitnik/api/responses/ItemMoveMessageResponse.java @@ -0,0 +1,10 @@ +package cz.jzitnik.api.responses; + +import cz.jzitnik.api.types.ItemType; +import lombok.Getter; + +@Getter +public class ItemMoveMessageResponse { + private ItemType itemType; + private String playerToName; +} diff --git a/frontend/src/main/java/cz/jzitnik/api/types/Message.java b/frontend/src/main/java/cz/jzitnik/api/types/Message.java index 8334408..d53794a 100644 --- a/frontend/src/main/java/cz/jzitnik/api/types/Message.java +++ b/frontend/src/main/java/cz/jzitnik/api/types/Message.java @@ -3,6 +3,7 @@ package cz.jzitnik.api.types; import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.ObjectMapper; import cz.jzitnik.api.ApiService; +import cz.jzitnik.api.responses.ItemMoveMessageResponse; import cz.jzitnik.utils.Cli; import lombok.Getter; import lombok.NoArgsConstructor; @@ -85,6 +86,17 @@ public class Message { yield "Hráč " + Cli.Colors.BLUE + author.getName() + Cli.Colors.RESET + " využil item unknown."; } } + case ITEM_MOVED -> { + ObjectMapper objectMapper = new ObjectMapper(); + + try { + var data = objectMapper.readValue(content, ItemMoveMessageResponse.class); + + yield "Hráč " + Cli.Colors.BLUE + author.getName() + Cli.Colors.RESET + " dal " + Cli.Colors.BLUE + data.getItemType() + Cli.Colors.RESET + " hráči " + Cli.Colors.BLUE + data.getPlayerToName() + Cli.Colors.RESET + "."; + } catch (JsonProcessingException e) { + yield "Hráč " + Cli.Colors.BLUE + author.getName() + Cli.Colors.RESET + " dal neznámý item neznámému hráči."; + } + } case KEY_FRAGMENT_HANDED_OVER -> "Hráč " + Cli.Colors.BLUE + author.getName() + Cli.Colors.RESET + " odevzdal " + Cli.Colors.BLUE + "Fragment klíče" + Cli.Colors.RESET + "."; }; } diff --git a/frontend/src/main/java/cz/jzitnik/api/types/MessageType.java b/frontend/src/main/java/cz/jzitnik/api/types/MessageType.java index 47c78f8..f576bba 100644 --- a/frontend/src/main/java/cz/jzitnik/api/types/MessageType.java +++ b/frontend/src/main/java/cz/jzitnik/api/types/MessageType.java @@ -7,5 +7,6 @@ public enum MessageType { LOST_ITEM, ITEM_USED, KEY_FRAGMENT_HANDED_OVER, + ITEM_MOVED, JOINED, } diff --git a/frontend/src/main/java/cz/jzitnik/game/CommandPalette.java b/frontend/src/main/java/cz/jzitnik/game/CommandPalette.java index 94fc6bd..22db564 100644 --- a/frontend/src/main/java/cz/jzitnik/game/CommandPalette.java +++ b/frontend/src/main/java/cz/jzitnik/game/CommandPalette.java @@ -9,6 +9,7 @@ import lombok.Setter; import java.io.File; import java.io.IOException; +import java.nio.file.Path; import java.util.ArrayList; import java.util.List; @@ -21,6 +22,7 @@ public class CommandPalette { USER_PROFILE, CHAT_OPEN, CHAT_SEND, + PLAYER_LIST_OPEN, CHECK_WINNABLE, EXIT, } @@ -68,7 +70,7 @@ public class CommandPalette { var configPath = ConfigPathProvider.getPath(); - File file = new File(configPath); + File file = new File(Path.of(configPath, "config.json").toString()); file.delete(); System.exit(0); @@ -81,6 +83,7 @@ public class CommandPalette { allCommands.add(new Command("Otevřít profil uživatele", CommandType.USER_PROFILE)); allCommands.add(new Command("Otevřít chat", CommandType.CHAT_OPEN)); allCommands.add(new Command("Odeslat zprávu do chatu", CommandType.CHAT_SEND)); + allCommands.add(new Command("Zobrazit list všech hráčů", CommandType.PLAYER_LIST_OPEN)); allCommands.add(new Command("Zkontrolovat vyhratelnost hry", CommandType.CHECK_WINNABLE)); allCommands.add(new Command("Odejít ze hry", CommandType.EXIT)); @@ -148,6 +151,12 @@ public class CommandPalette { Cli.info("Hra je dost možná nevyhratelná!"); } + yield displayIndex(); + } + case PLAYER_LIST_OPEN -> { + var playerList = new PlayerList(apiService, playerKey); + playerList.display(); + yield displayIndex(); } }; diff --git a/frontend/src/main/java/cz/jzitnik/game/Inventory.java b/frontend/src/main/java/cz/jzitnik/game/Inventory.java index 3c7fd52..82cc4cc 100644 --- a/frontend/src/main/java/cz/jzitnik/game/Inventory.java +++ b/frontend/src/main/java/cz/jzitnik/game/Inventory.java @@ -21,6 +21,11 @@ public class Inventory { System.out.println(); Cli.info("Váš inventář:"); + if (player.getInventory().isEmpty()) { + System.out.println("Inventář je prázdný!"); + return; + } + // Create a map to group items by ItemType and count occurrences Map itemCounts = new HashMap<>(); List usableItemList = new ArrayList<>(); diff --git a/frontend/src/main/java/cz/jzitnik/game/PlayerList.java b/frontend/src/main/java/cz/jzitnik/game/PlayerList.java new file mode 100644 index 0000000..505d6b6 --- /dev/null +++ b/frontend/src/main/java/cz/jzitnik/game/PlayerList.java @@ -0,0 +1,74 @@ +package cz.jzitnik.game; + +import cz.jzitnik.api.ApiService; +import cz.jzitnik.api.types.Item; +import cz.jzitnik.api.types.Player; +import cz.jzitnik.utils.Cli; +import lombok.AllArgsConstructor; +import org.apache.hc.core5.http.HttpStatus; + +import java.io.IOException; +import java.util.Arrays; + +@AllArgsConstructor +public class PlayerList { + private ApiService apiService; + private String playerKey; + + + public void display() throws IOException { + var response = apiService.getAllPlayers(playerKey).execute(); + var players = response.body().getData().get(); + + Cli.printHeader("List všech hráčů"); + + for (Player player1 : players) { + System.out.println("Jméno: " + Cli.Colors.BLUE + player1.getName() + Cli.Colors.RESET + ", V místnosti: " + Cli.Colors.BLUE + player1.getCurrentRoom() + Cli.Colors.RESET); + } + + if (players.size() == 1) { + // There is only one player so can't send item to another player + return; + } + + System.out.println("\n"); + var options = new String[] { "Předat item hráči", "Zavřít list hráčů"}; + + var selectedIndex = Cli.selectOptionIndex(Arrays.asList(options)); + + if (selectedIndex == 1) { + return; + } + + // Select item from inventory + var res = apiService.getMe(playerKey).execute(); + var me = res.body().getData().get(); + var inventory = me.getInventory(); + + if (inventory.isEmpty()) { + Cli.error("Nemáte žádné itemy v inventáři"); + return; + } + + var playerList = players.stream().filter(player -> !player.getId().equals(me.getId())).toList(); + var playerOptions = playerList.stream().map(Player::getName).toList(); + + System.out.println("\n\nVyberte hráče kterému chcete dát item"); + var selectedPlayerIndex = Cli.selectOptionIndex(playerOptions); + var selectedPlayer = playerList.get(selectedPlayerIndex); + + System.out.println("\n\nVyberte item který chcete dát hráči"); + var items = inventory.stream().map(Item::toString).toList(); + var selectedItemIndex = Cli.selectOptionIndex(items); + var selectedItem = inventory.get(selectedItemIndex); + + var finalResponse = apiService.sendItem(playerKey, selectedItem.getId(), selectedPlayer.getId()).execute(); + + if (finalResponse.code() == HttpStatus.SC_CONFLICT) { + Cli.error("Hráč " + Cli.Colors.BLUE + selectedPlayer.getName() + Cli.Colors.RESET + " má plný inventář!"); + return; + } + + Cli.success("Item byl úspěšně předán hráči " + Cli.Colors.BLUE + selectedPlayer.getName() + Cli.Colors.RESET + "."); + } +}