feat: Implemented water

This commit is contained in:
Jakub Žitník 2025-03-02 19:53:19 +01:00
parent 673e1c63f1
commit 451845b2bc
Signed by: jzitnik
GPG Key ID: C577A802A6AF4EF3
17 changed files with 291 additions and 4 deletions

View File

@ -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);

View File

@ -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<Block>[][] world = (List<Block>[][]) new ArrayList[256][512];
private final List<Block>[][] world = (List<Block>[][]) 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;

View File

@ -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"));

View File

@ -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 {
}

View File

@ -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",

View File

@ -25,6 +25,7 @@ public class Block {
private List<ToolVariant> toolVariants = new ArrayList<>();
private List<Item> drops = new ArrayList<>();
private Object data = null;
private boolean flowing = false;
private boolean isMob = false;
private int hp = 0;

View File

@ -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);
}
}

View File

@ -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<Block>[][] 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<>();
}
}
}

View File

@ -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();

View File

@ -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();

View File

@ -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();

View File

@ -0,0 +1,7 @@
package cz.jzitnik.game.logic;
import cz.jzitnik.game.Game;
public interface CustomLogicInterface {
void nextIteration(Game game);
}

View File

@ -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<CustomLogicInterface> 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<Class<?>> 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();
}
}
}
}
}

View File

@ -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;
}

View File

@ -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<WaterBlock> 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<Block>[][] world, int x, int y, int strength, Set<Point> 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<Block> blocks) {
return blocks.stream().allMatch(block -> block.getBlockId().equals("steve") || block.getBlockId().equals("air") || block.isMob());
}
}

View File

@ -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();
}
}

View File

@ -0,0 +1,25 @@