From 451845b2bce47daed8e183de7b058e8462d0946c Mon Sep 17 00:00:00 2001 From: jzitnik-dev Date: Sun, 2 Mar 2025 19:53:19 +0100 Subject: [PATCH] feat: Implemented water --- src/main/java/cz/jzitnik/Main.java | 7 ++ src/main/java/cz/jzitnik/game/Game.java | 5 +- .../java/cz/jzitnik/game/SpriteLoader.java | 2 + .../jzitnik/game/annotations/CustomLogic.java | 11 ++ .../game/crafting/recipes/BedRecipe.java | 2 +- .../java/cz/jzitnik/game/entities/Block.java | 1 + .../items/registry/blocks/WaterBlock.java | 19 ++++ .../jzitnik/game/generation/Generation.java | 3 +- .../handlers/place/DefaultPlaceHandler.java | 2 + .../place/handlers/BedPlaceHandler.java | 3 + .../place/handlers/DoorPlaceHandler.java | 2 + .../game/logic/CustomLogicInterface.java | 7 ++ .../game/logic/CustomLogicProvider.java | 39 +++++++ .../game/logic/services/water/WaterData.java | 10 ++ .../game/logic/services/water/WaterLogic.java | 102 ++++++++++++++++++ .../java/cz/jzitnik/game/sprites/Water.java | 55 ++++++++++ src/main/resources/textures/water.ans | 25 +++++ 17 files changed, 291 insertions(+), 4 deletions(-) create mode 100644 src/main/java/cz/jzitnik/game/annotations/CustomLogic.java create mode 100644 src/main/java/cz/jzitnik/game/entities/items/registry/blocks/WaterBlock.java create mode 100644 src/main/java/cz/jzitnik/game/logic/CustomLogicInterface.java create mode 100644 src/main/java/cz/jzitnik/game/logic/CustomLogicProvider.java create mode 100644 src/main/java/cz/jzitnik/game/logic/services/water/WaterData.java create mode 100644 src/main/java/cz/jzitnik/game/logic/services/water/WaterLogic.java create mode 100644 src/main/java/cz/jzitnik/game/sprites/Water.java create mode 100644 src/main/resources/textures/water.ans diff --git a/src/main/java/cz/jzitnik/Main.java b/src/main/java/cz/jzitnik/Main.java index 4e38ecd..58b2936 100644 --- a/src/main/java/cz/jzitnik/Main.java +++ b/src/main/java/cz/jzitnik/Main.java @@ -1,6 +1,7 @@ package cz.jzitnik; import cz.jzitnik.game.Game; +import cz.jzitnik.game.logic.CustomLogicProvider; import cz.jzitnik.game.mobs.EntityLogicProvider; import cz.jzitnik.game.threads.HealthRegenerationThread; import cz.jzitnik.game.threads.HungerDrainThread; @@ -35,6 +36,7 @@ public class Main { Thread healingThread = new HealthRegenerationThread(game.getPlayer()); Thread hungerDrainThread = new HungerDrainThread(game.getPlayer()); EntityLogicProvider entityLogicProvider = new EntityLogicProvider(); + CustomLogicProvider customLogicProvider = new CustomLogicProvider(); // Start all threads healingThread.start(); @@ -47,6 +49,11 @@ public class Main { } catch (Exception ignored) { // Yeah, yeah I know. Deal with it } + try { + customLogicProvider.update(game); + } catch (Exception e) { + e.printStackTrace(); + } if (game.getWindow() == Window.WORLD) { screenRenderer.render(game); diff --git a/src/main/java/cz/jzitnik/game/Game.java b/src/main/java/cz/jzitnik/game/Game.java index 3908885..826154e 100644 --- a/src/main/java/cz/jzitnik/game/Game.java +++ b/src/main/java/cz/jzitnik/game/Game.java @@ -24,11 +24,12 @@ import org.jline.terminal.Terminal; import java.util.ArrayList; import java.util.List; +import java.util.concurrent.CopyOnWriteArrayList; @Getter public class Game { @SuppressWarnings("unchecked") - private final List[][] world = (List[][]) new ArrayList[256][512]; + private final List[][] world = (List[][]) new CopyOnWriteArrayList[256][512]; private final Player player = new Player(); private boolean mining = false; @Setter @@ -339,7 +340,7 @@ public class Game { return; } - if (!blocks.stream().allMatch(block -> block.getBlockId().equals("air"))) { + if (!blocks.stream().allMatch(block -> block.getBlockId().equals("air") || block.isFlowing())) { RightClickHandlerProvider.handle(x, y, this, screenRenderer); screenRenderer.render(this); return; diff --git a/src/main/java/cz/jzitnik/game/SpriteLoader.java b/src/main/java/cz/jzitnik/game/SpriteLoader.java index d3013e2..596c614 100644 --- a/src/main/java/cz/jzitnik/game/SpriteLoader.java +++ b/src/main/java/cz/jzitnik/game/SpriteLoader.java @@ -13,6 +13,7 @@ public class SpriteLoader { // Blocks AIR, + WATER, DIRT, GRASS, STONE, @@ -124,6 +125,7 @@ public class SpriteLoader { // Block SPRITES_MAP.put(SPRITES.AIR, new Air()); + SPRITES_MAP.put(SPRITES.WATER, new Water()); SPRITES_MAP.put(SPRITES.DIRT, new SimpleSprite("dirt.ans")); SPRITES_MAP.put(SPRITES.GRASS, new SimpleSprite("grass.ans")); SPRITES_MAP.put(SPRITES.STONE, new SimpleSprite("stone.ans")); diff --git a/src/main/java/cz/jzitnik/game/annotations/CustomLogic.java b/src/main/java/cz/jzitnik/game/annotations/CustomLogic.java new file mode 100644 index 0000000..00b57af --- /dev/null +++ b/src/main/java/cz/jzitnik/game/annotations/CustomLogic.java @@ -0,0 +1,11 @@ +package cz.jzitnik.game.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 CustomLogic { +} diff --git a/src/main/java/cz/jzitnik/game/crafting/recipes/BedRecipe.java b/src/main/java/cz/jzitnik/game/crafting/recipes/BedRecipe.java index 61c4591..34ff870 100644 --- a/src/main/java/cz/jzitnik/game/crafting/recipes/BedRecipe.java +++ b/src/main/java/cz/jzitnik/game/crafting/recipes/BedRecipe.java @@ -5,7 +5,7 @@ import cz.jzitnik.game.annotations.CraftingRecipeRegistry; @CraftingRecipeRegistry( recipe = { "^.*_wool$", "^.*_wool$", "^.*_wool$", - "oak_planks", "oak_planks", "oak_planks", + "^oak_planks$", "^oak_planks$", "^oak_planks$", "_", "_", "_" }, result = "bed", diff --git a/src/main/java/cz/jzitnik/game/entities/Block.java b/src/main/java/cz/jzitnik/game/entities/Block.java index b8a2f3b..3473143 100644 --- a/src/main/java/cz/jzitnik/game/entities/Block.java +++ b/src/main/java/cz/jzitnik/game/entities/Block.java @@ -25,6 +25,7 @@ public class Block { private List toolVariants = new ArrayList<>(); private List drops = new ArrayList<>(); private Object data = null; + private boolean flowing = false; private boolean isMob = false; private int hp = 0; diff --git a/src/main/java/cz/jzitnik/game/entities/items/registry/blocks/WaterBlock.java b/src/main/java/cz/jzitnik/game/entities/items/registry/blocks/WaterBlock.java new file mode 100644 index 0000000..a9a9e5c --- /dev/null +++ b/src/main/java/cz/jzitnik/game/entities/items/registry/blocks/WaterBlock.java @@ -0,0 +1,19 @@ +package cz.jzitnik.game.entities.items.registry.blocks; + +import cz.jzitnik.game.SpriteLoader; +import cz.jzitnik.game.annotations.BlockRegistry; +import cz.jzitnik.game.entities.Block; +import cz.jzitnik.game.logic.services.water.WaterData; +import cz.jzitnik.game.sprites.Water; + +@BlockRegistry("water") +public class WaterBlock extends Block { + public WaterBlock() { + super("water", SpriteLoader.SPRITES.WATER); + setMineable(false); + setSpriteState(Water.WaterState.FIRST); + setData(new WaterData()); + setFlowing(true); + setGhost(true); + } +} diff --git a/src/main/java/cz/jzitnik/game/generation/Generation.java b/src/main/java/cz/jzitnik/game/generation/Generation.java index 39e7934..3118092 100644 --- a/src/main/java/cz/jzitnik/game/generation/Generation.java +++ b/src/main/java/cz/jzitnik/game/generation/Generation.java @@ -10,6 +10,7 @@ import cz.jzitnik.game.sprites.Steve; import java.util.ArrayList; import java.util.List; import java.util.Random; +import java.util.concurrent.CopyOnWriteArrayList; public class Generation { public static void generateWorld(Game game) { @@ -39,7 +40,7 @@ public class Generation { private static void initializeWorld(List[][] world) { for (int i = 0; i < 256; i++) { for (int j = 0; j < 512; j++) { - world[i][j] = new ArrayList<>(); + world[i][j] = new CopyOnWriteArrayList<>(); } } } diff --git a/src/main/java/cz/jzitnik/game/handlers/place/DefaultPlaceHandler.java b/src/main/java/cz/jzitnik/game/handlers/place/DefaultPlaceHandler.java index 2d8681a..37d9403 100644 --- a/src/main/java/cz/jzitnik/game/handlers/place/DefaultPlaceHandler.java +++ b/src/main/java/cz/jzitnik/game/handlers/place/DefaultPlaceHandler.java @@ -1,6 +1,7 @@ package cz.jzitnik.game.handlers.place; import cz.jzitnik.game.Game; +import cz.jzitnik.game.entities.Block; public class DefaultPlaceHandler implements CustomPlaceHandler { @Override @@ -9,6 +10,7 @@ public class DefaultPlaceHandler implements CustomPlaceHandler { var inventory = game.getInventory(); blocks.add(inventory.getItemInHand().get().getBlock().get()); + blocks.removeAll(blocks.stream().filter(Block::isFlowing).toList()); inventory.decreaseItemInHand(); diff --git a/src/main/java/cz/jzitnik/game/handlers/place/handlers/BedPlaceHandler.java b/src/main/java/cz/jzitnik/game/handlers/place/handlers/BedPlaceHandler.java index 5a413ce..17f1c45 100644 --- a/src/main/java/cz/jzitnik/game/handlers/place/handlers/BedPlaceHandler.java +++ b/src/main/java/cz/jzitnik/game/handlers/place/handlers/BedPlaceHandler.java @@ -24,6 +24,7 @@ public class BedPlaceHandler implements CustomPlaceHandler { Block block2 = ItemBlockSupplier.getBlock("bed"); block2.setSpriteState(Bed.BedState.RIGHT); blocksRight.add(block2); + blocksRight.removeAll(blocksRight.stream().filter(Block::isFlowing).toList()); Block block = inventory.getItemInHand().get().getBlock().get(); block.setSpriteState(Bed.BedState.LEFT); @@ -32,11 +33,13 @@ public class BedPlaceHandler implements CustomPlaceHandler { Block block2 = ItemBlockSupplier.getBlock("bed"); block2.setSpriteState(Bed.BedState.LEFT); blocksLeft.add(block2); + blocksLeft.removeAll(blocksLeft.stream().filter(Block::isFlowing).toList()); Block block = inventory.getItemInHand().get().getBlock().get(); block.setSpriteState(Bed.BedState.RIGHT); blocks.add(block); } + blocks.removeAll(blocks.stream().filter(Block::isFlowing).toList()); inventory.decreaseItemInHand(); diff --git a/src/main/java/cz/jzitnik/game/handlers/place/handlers/DoorPlaceHandler.java b/src/main/java/cz/jzitnik/game/handlers/place/handlers/DoorPlaceHandler.java index 61d28f9..4acf776 100644 --- a/src/main/java/cz/jzitnik/game/handlers/place/handlers/DoorPlaceHandler.java +++ b/src/main/java/cz/jzitnik/game/handlers/place/handlers/DoorPlaceHandler.java @@ -23,10 +23,12 @@ public class DoorPlaceHandler implements CustomPlaceHandler { Block block = inventory.getItemInHand().get().getBlock().get(); block.setSpriteState(OakDoor.OakDoorState.BOTTOMCLOSED); blocks.add(block); + blocks.removeAll(blocks.stream().filter(Block::isFlowing).toList()); Block block2 = ItemBlockSupplier.getBlock("oak_door"); block2.setSpriteState(OakDoor.OakDoorState.TOPCLOSED); blocksTop.add(block2); + blocksTop.removeAll(blocksTop.stream().filter(Block::isFlowing).toList()); inventory.decreaseItemInHand(); diff --git a/src/main/java/cz/jzitnik/game/logic/CustomLogicInterface.java b/src/main/java/cz/jzitnik/game/logic/CustomLogicInterface.java new file mode 100644 index 0000000..e0fc0f5 --- /dev/null +++ b/src/main/java/cz/jzitnik/game/logic/CustomLogicInterface.java @@ -0,0 +1,7 @@ +package cz.jzitnik.game.logic; + +import cz.jzitnik.game.Game; + +public interface CustomLogicInterface { + void nextIteration(Game game); +} diff --git a/src/main/java/cz/jzitnik/game/logic/CustomLogicProvider.java b/src/main/java/cz/jzitnik/game/logic/CustomLogicProvider.java new file mode 100644 index 0000000..d55e359 --- /dev/null +++ b/src/main/java/cz/jzitnik/game/logic/CustomLogicProvider.java @@ -0,0 +1,39 @@ +package cz.jzitnik.game.logic; + +import java.util.ArrayList; +import java.util.List; +import java.util.Set; + +import cz.jzitnik.game.Game; +import cz.jzitnik.game.annotations.CustomLogic; +import org.reflections.Reflections; + +public class CustomLogicProvider { + private final List logicList = new ArrayList<>(); + + public void update(Game game) { + for (CustomLogicInterface logicInterface : logicList) { + logicInterface.nextIteration(game); + } + } + + public CustomLogicProvider() { + registerHandlers(); + } + + private void registerHandlers() { + Reflections reflections = new Reflections("cz.jzitnik.game.logic.services"); + Set> handlerClasses = reflections.getTypesAnnotatedWith(CustomLogic.class); + + for (Class clazz : handlerClasses) { + if (CustomLogicInterface.class.isAssignableFrom(clazz)) { + try { + CustomLogicInterface instance = (CustomLogicInterface) clazz.getDeclaredConstructor().newInstance(); + logicList.add(instance); + } catch (Exception e) { + e.printStackTrace(); + } + } + } + } +} diff --git a/src/main/java/cz/jzitnik/game/logic/services/water/WaterData.java b/src/main/java/cz/jzitnik/game/logic/services/water/WaterData.java new file mode 100644 index 0000000..2f7fe31 --- /dev/null +++ b/src/main/java/cz/jzitnik/game/logic/services/water/WaterData.java @@ -0,0 +1,10 @@ +package cz.jzitnik.game.logic.services.water; + +import lombok.Getter; +import lombok.Setter; + +@Getter +@Setter +public class WaterData { + private boolean isSource = true; +} diff --git a/src/main/java/cz/jzitnik/game/logic/services/water/WaterLogic.java b/src/main/java/cz/jzitnik/game/logic/services/water/WaterLogic.java new file mode 100644 index 0000000..ec545d4 --- /dev/null +++ b/src/main/java/cz/jzitnik/game/logic/services/water/WaterLogic.java @@ -0,0 +1,102 @@ +package cz.jzitnik.game.logic.services.water; + +import cz.jzitnik.game.Game; +import cz.jzitnik.game.annotations.CustomLogic; +import cz.jzitnik.game.entities.Block; +import cz.jzitnik.game.entities.items.ItemBlockSupplier; +import cz.jzitnik.game.logic.CustomLogicInterface; +import cz.jzitnik.game.sprites.Water; +import lombok.AllArgsConstructor; +import lombok.Getter; + +import java.awt.*; +import java.util.ArrayList; +import java.util.HashSet; +import java.util.List; +import java.util.Set; + +@CustomLogic +public class WaterLogic implements CustomLogicInterface { + private static final int RADIUS = 20; + + @AllArgsConstructor + @Getter + private static class WaterBlock { + private Block block; + private int x; + private int y; + } + + @Override + public void nextIteration(Game game) { + int[] data = game.getPlayerCords(); + var world = game.getWorld(); + int playerX = data[0]; + int playerY = data[1]; + + int startX = Math.max(0, playerX - RADIUS); + int startY = Math.max(0, playerY - RADIUS); + int endX = Math.min(world[0].length - 1, playerX + RADIUS); + int endY = Math.min(world.length - 1, playerY + RADIUS); + + List sourceBlocks = new ArrayList<>(); + + for (int y = startY; y <= endY; y++) { + for (int x = startX; x <= endX; x++) { + var blocks = world[y][x]; + + var waterSourceBlocks = blocks.stream().filter(block -> block.getBlockId().equals("water") && ((WaterData) block.getData()).isSource()).toList(); + if (!waterSourceBlocks.isEmpty()) { + sourceBlocks.add(new WaterBlock(waterSourceBlocks.getFirst(), x, y)); + } + + world[y][x].removeAll(blocks.stream().filter(i -> i.getBlockId().equals("water") && !((WaterData) i.getData()).isSource()).toList()); + } + } + + + for (WaterBlock sourceBlock : sourceBlocks) { + int x = sourceBlock.getX(); + int y = sourceBlock.getY(); + + flow(world, x, y, 5, new HashSet<>()); + } + } + + public void flow(List[][] world, int x, int y, int strength, Set visited) { + if (y + 1 < world.length && waterCanFlow(world[y+1][x])) { + Block newWater = ItemBlockSupplier.getBlock("water"); + newWater.setSpriteState(Water.WaterState.get(5)); + ((WaterData) newWater.getData()).setSource(false); + world[y+1][x].add(newWater); + flow(world, x, y + 1, 5, visited); + return; + } + + if (strength == 1 || visited.contains(new Point(x, y))) { + return; + } + visited.add(new Point(x, y)); + + if (x - 1 >= 0 && waterCanFlow(world[y][x-1])) { + Block newWater = ItemBlockSupplier.getBlock("water"); + newWater.setSpriteState(Water.WaterState.get(strength - 1)); + ((WaterData) newWater.getData()).setSource(false); + world[y][x-1].add(newWater); + flow(world, x - 1, y, strength - 1, visited); + } + + if (x + 1 < world[y].length && waterCanFlow(world[y][x+1])) { + Block newWater = ItemBlockSupplier.getBlock("water"); + newWater.setSpriteState(Water.WaterState.get(strength - 1)); + ((WaterData) newWater.getData()).setSource(false); + world[y][x+1].add(newWater); + flow(world, x + 1, y, strength - 1, visited); + } + } + + + private boolean waterCanFlow(List blocks) { + return blocks.stream().allMatch(block -> block.getBlockId().equals("steve") || block.getBlockId().equals("air") || block.isMob()); + } +} diff --git a/src/main/java/cz/jzitnik/game/sprites/Water.java b/src/main/java/cz/jzitnik/game/sprites/Water.java new file mode 100644 index 0000000..9a5af08 --- /dev/null +++ b/src/main/java/cz/jzitnik/game/sprites/Water.java @@ -0,0 +1,55 @@ +package cz.jzitnik.game.sprites; + +import cz.jzitnik.tui.ResourceLoader; +import cz.jzitnik.tui.Sprite; + +public class Water extends Sprite { + public enum WaterState { + FIRST, + SECOND, + THIRD, + FOURTH, + FIFTH; + + public static WaterState get(int x) { + return switch (x) { + case 5 -> FIRST; + case 4 -> SECOND; + case 3 -> THIRD; + case 2 -> FOURTH; + case 1 -> FIFTH; + default -> throw new IllegalStateException("Unexpected value: " + x); + }; + } + } + + public String getSprite() { + return getSprite(WaterState.FIRST); + } + + public String getSprite(Enum e) { + String[] resource = ResourceLoader.loadResource("water.ans").split("\n"); + + int numberFormTop = switch (e) { + case WaterState.FIRST -> 0; + case WaterState.SECOND -> 5; + case WaterState.THIRD -> 10; + case WaterState.FOURTH -> 15; + case WaterState.FIFTH -> 20; + default -> throw new IllegalStateException("Unexpected value: " + e); + }; + + StringBuilder stringBuilder = new StringBuilder(); + + for (int i = 0; i < 25; i++) { + if (i < numberFormTop) { + stringBuilder.append("\033[49m ".repeat(50)); + } else { + stringBuilder.append(resource[i]); + } + stringBuilder.append("\n"); + } + + return stringBuilder.toString(); + } +} diff --git a/src/main/resources/textures/water.ans b/src/main/resources/textures/water.ans new file mode 100644 index 0000000..81809bd --- /dev/null +++ b/src/main/resources/textures/water.ans @@ -0,0 +1,25 @@ +                    +                          +                    +                       +                        +                     +                   +                    +                  +                     +                    +                       +                    +                    +                    +                     +                  +                     +                   +               +               +                       +                 +              +