From 4050ede100be97afe0030f3731e3f0f74d70fa1a Mon Sep 17 00:00:00 2001 From: KrystilizeNevaDies Date: Sun, 1 May 2022 17:47:58 +1000 Subject: [PATCH 1/2] Create initial WaveFunctionCollapse impl --- .../minestom/arena/feature/CombatFeature.java | 7 +- .../net/minestom/arena/game/ArenaCommand.java | 72 +- .../arena/game/SingleInstanceArena.java | 4 +- .../net/minestom/arena/game/mob/ArenaMob.java | 8 +- .../arena/game/procedural/GenerationData.java | 654 ++++++++++++++++++ .../game/procedural/ProceduralArena.java | 69 ++ .../WaveFunctionCollapseGenerator.java | 292 ++++++++ .../java/net/minestom/arena/group/Group.java | 2 + .../minestom/arena/group/GroupCommand.java | 2 +- 9 files changed, 1079 insertions(+), 31 deletions(-) create mode 100644 src/main/java/net/minestom/arena/game/procedural/GenerationData.java create mode 100644 src/main/java/net/minestom/arena/game/procedural/ProceduralArena.java create mode 100644 src/main/java/net/minestom/arena/game/procedural/WaveFunctionCollapseGenerator.java diff --git a/src/main/java/net/minestom/arena/feature/CombatFeature.java b/src/main/java/net/minestom/arena/feature/CombatFeature.java index 9e22a38..23339a3 100644 --- a/src/main/java/net/minestom/arena/feature/CombatFeature.java +++ b/src/main/java/net/minestom/arena/feature/CombatFeature.java @@ -26,11 +26,12 @@ import java.util.function.ToLongFunction; /** - * @param playerCombat Allow player combat - * @param damageFunction Uses the return value as damage to apply (in lambda arg 1 is attacker, arg 2 is victim) + * @param playerCombat Allow player combat + * @param damageFunction Uses the return value as damage to apply (in lambda arg 1 is attacker, arg 2 is victim) * @param invulnerabilityFunction Uses the return value as time an entity is invulnerable after getting attacked (in lambda arg 1 is victim) */ -record CombatFeature(boolean playerCombat, ToDoubleBiFunction damageFunction, ToLongFunction invulnerabilityFunction) implements Feature { +record CombatFeature(boolean playerCombat, ToDoubleBiFunction damageFunction, + ToLongFunction invulnerabilityFunction) implements Feature { private static final Tag INVULNERABLE_UNTIL_TAG = Tag.Long("invulnerable_until").defaultValue(0L); private void takeKnockback(Entity target, Entity source) { diff --git a/src/main/java/net/minestom/arena/game/ArenaCommand.java b/src/main/java/net/minestom/arena/game/ArenaCommand.java index 9c2ff58..7863c9e 100644 --- a/src/main/java/net/minestom/arena/game/ArenaCommand.java +++ b/src/main/java/net/minestom/arena/game/ArenaCommand.java @@ -6,6 +6,7 @@ import net.minestom.arena.Lobby; import net.minestom.arena.Messenger; import net.minestom.arena.game.mob.MobArena; +import net.minestom.arena.game.procedural.ProceduralArena; import net.minestom.arena.group.Group; import net.minestom.arena.utils.CommandUtils; import net.minestom.arena.utils.ItemUtils; @@ -17,15 +18,42 @@ import net.minestom.server.item.ItemStack; import net.minestom.server.item.Material; import net.minestom.server.tag.Tag; +import org.jetbrains.annotations.NotNull; import java.util.List; -import java.util.Map; -import java.util.concurrent.atomic.AtomicInteger; +import java.util.function.Consumer; import java.util.function.Function; public final class ArenaCommand extends Command { - private static final Map> ARENAS = Map.of( - "mob", MobArena::new); + + // In order to add a new arena, add it to this enum. + private enum ArenaType { + MOB(Material.ZOMBIE_HEAD, + builder -> builder.displayName(Component.text("Mob Arena", NamedTextColor.GREEN)), + MobArena::new), + PROCEDURAL(Material.BONE_BLOCK, + builder -> builder.displayName(Component.text("Procedural", NamedTextColor.RED)), + ProceduralArena::new); + + private final ItemStack menuItem; + private final Function arenaCreator; + + ArenaType(Material material, Consumer builderConsumer, + Function arenaCreator) { + this.arenaCreator = arenaCreator; + ItemStack.Builder builder = ItemStack.builder(material); + builderConsumer.accept(builder); + this.menuItem = builder.build(); + } + + public @NotNull ItemStack menuItem() { + return menuItem; + } + + public Arena startNew(Group group) { + return arenaCreator.apply(group); + } + } public ArenaCommand() { super("arena"); @@ -34,12 +62,13 @@ public ArenaCommand() { setDefaultExecutor((sender, context) -> ((Player) sender).openInventory(new ArenaInventory())); - addSyntax((sender, context) -> - play((Player) sender, context.get("type")), - ArgumentType.Word("type").from(ARENAS.keySet().toArray(String[]::new))); + addSyntax( + (sender, context) -> play((Player) sender, context.get("type")), + ArgumentType.Enum("type", ArenaType.class) + ); } - private static void play(Player player, String type) { + private static void play(Player player, ArenaType type) { if (player.getInstance() != Lobby.INSTANCE) { Messenger.warn(player, "You are not in the lobby! Join the lobby first."); return; @@ -49,20 +78,16 @@ private static void play(Player player, String type) { Messenger.warn(player, "You are not the leader of your group!"); return; } - Arena arena = ARENAS.get(type).apply(group); + Arena arena = type.startNew(group); arena.init().thenRun(() -> group.members().forEach(Player::refreshCommands)); } private static class ArenaInventory extends Inventory { - private static final Tag ARENA_TAG = Tag.String("arena"); + private static final Tag ARENA_TAG = Tag.String("arena").map(ArenaType::valueOf, ArenaType::name); private static final ItemStack HEADER = ItemUtils.stripItalics(ItemStack.builder(Material.IRON_BARS) .displayName(Component.text("Arena", NamedTextColor.RED)) .lore(Component.text("Select an arena to play in", NamedTextColor.GRAY)) .build()); - private static final Map ARENA_ITEM = Map.of( - ItemStack.builder(Material.ZOMBIE_HEAD) - .displayName(Component.text("Mob Arena", NamedTextColor.GREEN)) - .build(), "mob"); public ArenaInventory() { super(InventoryType.CHEST_4_ROW, Component.text("Arena")); @@ -70,12 +95,17 @@ public ArenaInventory() { setItemStack(4, HEADER); setItemStack(31, Items.CLOSE); - AtomicInteger i = new AtomicInteger(13 - ARENA_ITEM.size() / 2); - ARENA_ITEM.forEach((item, arena) -> - setItemStack(i.getAndIncrement(), ItemUtils.stripItalics(item.withLore(List.of( - Component.text("Click to play in the " + arena + " arena", NamedTextColor.GRAY)) - ).withTag(ARENA_TAG, arena))) - ); + int i = 13 - ArenaType.values().length / 2; + for (ArenaType type : ArenaType.values()) { + ItemStack item = type.menuItem(); + setItemStack( + i++, + ItemUtils.stripItalics( + item.withLore(List.of(Component.text("Click to play in the " + type.name() + " arena", NamedTextColor.GRAY))) + .withTag(ARENA_TAG, type) + ) + ); + } addInventoryCondition((player, slot, c, result) -> { result.setCancel(true); @@ -85,7 +115,7 @@ public ArenaInventory() { return; } - final String arena = result.getClickedItem().getTag(ARENA_TAG); + ArenaType arena = result.getClickedItem().getTag(ARENA_TAG); if (arena != null) { player.closeInventory(); diff --git a/src/main/java/net/minestom/arena/game/SingleInstanceArena.java b/src/main/java/net/minestom/arena/game/SingleInstanceArena.java index 2f20f46..ac0dd80 100644 --- a/src/main/java/net/minestom/arena/game/SingleInstanceArena.java +++ b/src/main/java/net/minestom/arena/game/SingleInstanceArena.java @@ -41,8 +41,8 @@ public interface SingleInstanceArena extends Arena { CompletableFuture[] futures = group().members().stream() - .map(player -> player.setInstance(instance, spawnPosition(player))) - .toArray(CompletableFuture[]::new); + .map(player -> player.setInstance(instance, spawnPosition(player))) + .toArray(CompletableFuture[]::new); return CompletableFuture.allOf(futures).thenRun(this::start); } diff --git a/src/main/java/net/minestom/arena/game/mob/ArenaMob.java b/src/main/java/net/minestom/arena/game/mob/ArenaMob.java index 86f9608..074f1ff 100644 --- a/src/main/java/net/minestom/arena/game/mob/ArenaMob.java +++ b/src/main/java/net/minestom/arena/game/mob/ArenaMob.java @@ -28,9 +28,9 @@ public ArenaMob(@NotNull EntityType entityType, int stage) { setCustomName(generateHealthBar(getMaxHealth(), getHealth())); setCustomNameVisible(true); eventNode().addListener(EntityDamageEvent.class, event -> - setCustomName(generateHealthBar(getMaxHealth(), getHealth()))) - .addListener(EntityDeathEvent.class, event -> - setCustomName(generateHealthBar(getMaxHealth(), 0))); + setCustomName(generateHealthBar(getMaxHealth(), getHealth()))) + .addListener(EntityDeathEvent.class, event -> + setCustomName(generateHealthBar(getMaxHealth(), 0))); } @Contract(pure = true) @@ -44,7 +44,7 @@ public ArenaMob(@NotNull EntityType entityType, int stage) { NamedTextColor.RED )).append(Component.text(CHARACTERS.get((int) Math.round( (charHealth - Math.floor(charHealth)) // number from 0-1 - * (CHARACTERS.size() - 1) // indexes start at 0 + * (CHARACTERS.size() - 1) // indexes start at 0 )), NamedTextColor.YELLOW)) .append(Component.text("]", NamedTextColor.DARK_GRAY)) .build(); diff --git a/src/main/java/net/minestom/arena/game/procedural/GenerationData.java b/src/main/java/net/minestom/arena/game/procedural/GenerationData.java new file mode 100644 index 0000000..238f758 --- /dev/null +++ b/src/main/java/net/minestom/arena/game/procedural/GenerationData.java @@ -0,0 +1,654 @@ +package net.minestom.arena.game.procedural; + +import net.minestom.server.coordinate.Point; +import net.minestom.server.instance.block.Block; +import net.minestom.server.utils.Direction; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import java.util.*; +import java.util.function.Consumer; + +import static net.minestom.server.instance.block.Block.*; + +public class GenerationData { + + public static final List ALL_BLOCK_GROUPS = List.of(BlockGroups.values()); + public static final double MAX_DISTANCE = 100; + public static final BlockGroup AIR = BlockGroups.AIR; + + /** + * Blockgroups are 2x2 sections of the world. + * The order of the blocks is as follows: + * 0 -> x+, y+, z+ + * 1 -> x+, y+, z- + * 2 -> x+, y-, z+ + * 3 -> x+, y-, z- + * 4 -> x-, y+, z+ + * 5 -> x-, y+, z- + * 6 -> x-, y-, z+ + * 7 -> x-, y-, z- + *

+ * To help visualise this I suggest the use of a 2x2x2 rubiks cube. + * Help the cube with white facing up, and green facing towards you. + * 0 and 1 are the two corners between white and red + * 2 and 3 are the two corners between yellow and red + * 4 and 5 are the two corners between white and orange + * 6 and 7 are the two corners between yellow and orange + *

+ * Note that the piece in each pair that is closest to you is first. + *

+ * null blocks are valid, they mean they will not replace previous blocks if applicable + */ + interface BlockGroup { + /** + * @param direction the direction to check + * @return the blockgroups that are allowed to be adjacent to this blockgroup in the specified direction + */ + Set allowsTo(Direction direction); + + void place(Setter setter, int x, int y, int z); + + default void place(Setter setter, Point point) { + place(setter, point.blockX(), point.blockY(), point.blockZ()); + } + } + + // All the blockgroups + private enum BlockGroups implements BlockGroup { + AIR( + null, null, + null, null, + null, null, + null, null + ), + BLANK( + null, null, + null, null, + null, null, + null, null + ), + COBBLE_PATH_LEFT( + null, null, + COBBLESTONE, COBBLESTONE, + COBBLESTONE_WALL, COBBLESTONE_WALL, + COBBLESTONE, COBBLESTONE + ), + COBBLE_PATH( + null, null, + COBBLESTONE, COBBLESTONE, + null, null, + COBBLESTONE, COBBLESTONE + ), + COBBLE_PATH_RIGHT( + COBBLESTONE_WALL, COBBLESTONE_WALL, + COBBLESTONE, COBBLESTONE, + null, null, + COBBLESTONE, COBBLESTONE + ), + ; + + // Connections + private static final Map group2Connections = new HashMap<>(); + + static { + // AIR connects to everything + connections(AIR, towards -> towards.north().east().south().west().up().down().apply(BlockGroups.values())); + + // Blank + connections(BLANK, towards -> { + }); + + // Cobble paths + connections(COBBLE_PATH_LEFT, towards -> { + towards.right(COBBLE_PATH, COBBLE_PATH_RIGHT); + towards.front().back().apply(COBBLE_PATH_LEFT); + }); + connections(COBBLE_PATH, towards -> { + towards.horizontal().apply(COBBLE_PATH); + towards.left(COBBLE_PATH_LEFT); + towards.right(COBBLE_PATH_RIGHT); + }); + connections(COBBLE_PATH_RIGHT, towards -> { + towards.left(COBBLE_PATH_LEFT, COBBLE_PATH); + towards.front().back().apply(COBBLE_PATH_RIGHT); + }); + } + + // Fields + private final Block block0; + private final Block block1; + private final Block block2; + private final Block block3; + private final Block block4; + private final Block block5; + private final Block block6; + private final Block block7; + private @Nullable Connections connections; + + BlockGroups(Block block0, Block block1, Block block2, Block block3, + Block block4, Block block5, Block block6, Block block7) { + this.block0 = block0; + this.block1 = block1; + this.block2 = block2; + this.block3 = block3; + this.block4 = block4; + this.block5 = block5; + this.block6 = block6; + this.block7 = block7; + } + + @Override + public Set allowsTo(Direction direction) { + return switch (direction) { + case NORTH -> connections().north; + case SOUTH -> connections().south; + case EAST -> connections().east; + case WEST -> connections().west; + case UP -> connections().up; + case DOWN -> connections().down; + }; + } + + @Override + public void place(Setter setter, int x, int y, int z) { + /* + * 0 -> x+, y+, z+ + * 1 -> x+, y+, z- + * 2 -> x+, y-, z+ + * 3 -> x+, y-, z- + * 4 -> x-, y+, z+ + * 5 -> x-, y+, z- + * 6 -> x-, y-, z+ + * 7 -> x-, y-, z- + */ + int xOffset = x * 2; + int yOffset = y * 2; + int zOffset = z * 2; + + if (block0 != null) setter.setBlock(xOffset + 1, yOffset + 1, zOffset + 1, block0); + if (block1 != null) setter.setBlock(xOffset + 1, yOffset + 1, zOffset + 0, block1); + if (block2 != null) setter.setBlock(xOffset + 1, yOffset + 0, zOffset + 1, block2); + if (block3 != null) setter.setBlock(xOffset + 1, yOffset + 0, zOffset + 0, block3); + if (block4 != null) setter.setBlock(xOffset + 0, yOffset + 1, zOffset + 1, block4); + if (block5 != null) setter.setBlock(xOffset + 0, yOffset + 1, zOffset + 0, block5); + if (block6 != null) setter.setBlock(xOffset + 0, yOffset + 0, zOffset + 1, block6); + if (block7 != null) setter.setBlock(xOffset + 0, yOffset + 0, zOffset + 0, block7); + } + + private @NotNull Connections connections() { + if (connections == null) { + connections = group2Connections.get(this); + if (connections == null) { + throw new IllegalStateException("Connections not registered for " + this); + } + } + return connections; + } + + private static void connections(BlockGroup group, Consumer consumer) { + Connections connections = group2Connections.computeIfAbsent(group, ignored -> new Connections()); + + // Define the registry + ConnectionRegistry registry = (direction, groups) -> { + boolean result = false; + Set registered = switch (direction) { + case UP -> connections.up; + case DOWN -> connections.down; + case NORTH -> connections.north; + case SOUTH -> connections.south; + case EAST -> connections.east; + case WEST -> connections.west; + }; + for (BlockGroup g : groups) { + result |= registered.add(g); + } + return result; + }; + + // Apply the consumer + consumer.accept(registry); + } + + public static class Connections { + private final Set east = new HashSet<>(); + private final Set west = new HashSet<>(); + private final Set up = new HashSet<>(); + private final Set down = new HashSet<>(); + private final Set south = new HashSet<>(); + private final Set north = new HashSet<>(); + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + Connections that = (Connections) o; + return Objects.equals(east, that.east) && + Objects.equals(west, that.west) && + Objects.equals(up, that.up) && + Objects.equals(down, that.down) && + Objects.equals(south, that.south) && + Objects.equals(north, that.north); + } + + @Override + public int hashCode() { + return Objects.hash(east, west, up, down, south, north); + } + } + } + + @SuppressWarnings("unused") + private interface ConnectionRegistry { + + /** + * Register a connection from this blockgroup to the given blockgroups in the given direction. + * + * @param group The blockgroups to connect to. + * @return True if the connection was registered, false if the connection was already registered. + */ + boolean towards(@NotNull Direction direction, @NotNull BlockGroup... group); + + /** + * Creates a new connection builder with the direction set to the given direction. + * + * @return The new connection builder. + */ + default @NotNull ConnectionBuilder towards(@NotNull Direction direction) { + return new ConnectionBuilder(this).direction(direction); + } + + /** + * Register a connection from this blockgroup to the given blockgroups in the north direction. + * + * @param group The blockgroups to connect to. + * @return True if the connection was registered, false if the connection was already registered. + */ + default boolean north(@NotNull BlockGroup... group) { + return towards(Direction.NORTH, group); + } + + /** + * Creates a new connection builder with the direction set to the north direction. + * + * @return The new connection builder. + */ + default @NotNull ConnectionBuilder north() { + return towards(Direction.NORTH); + } + + /** + * Register a connection from this blockgroup to the given blockgroups in the east direction. + * + * @param group The blockgroups to connect to. + * @return True if the connection was registered, false if the connection was already registered. + */ + default boolean east(@NotNull BlockGroup... group) { + return towards(Direction.EAST, group); + } + + /** + * Creates a new connection builder with the direction set to the east direction. + * + * @return The new connection builder. + */ + default @NotNull ConnectionBuilder east() { + return towards(Direction.EAST); + } + + /** + * Register a connection from this blockgroup to the given blockgroups in the south direction. + * + * @param group The blockgroups to connect to. + * @return True if the connection was registered, false if the connection was already registered. + */ + default boolean south(@NotNull BlockGroup... group) { + return towards(Direction.SOUTH, group); + } + + /** + * Creates a new connection builder with the direction set to the south direction. + * + * @return The new connection builder. + */ + default @NotNull ConnectionBuilder south() { + return towards(Direction.SOUTH); + } + + /** + * Register a connection from this blockgroup to the given blockgroups in the west direction. + * + * @param group The blockgroups to connect to. + * @return True if the connection was registered, false if the connection was already registered. + */ + default boolean west(@NotNull BlockGroup... group) { + return towards(Direction.WEST, group); + } + + /** + * Creates a new connection builder with the direction set to the west direction. + * + * @return The new connection builder. + */ + default @NotNull ConnectionBuilder west() { + return towards(Direction.WEST); + } + + /** + * Register a connection from this blockgroup to the given blockgroups in the up direction. + * + * @param group The blockgroups to connect to. + * @return True if the connection was registered, false if the connection was already registered. + */ + default boolean up(@NotNull BlockGroup... group) { + return towards(Direction.UP, group); + } + + /** + * Creates a new connection builder with the direction set to the up direction. + * + * @return The new connection builder. + */ + default @NotNull ConnectionBuilder up() { + return towards(Direction.UP); + } + + /** + * Register a connection from this blockgroup to the given blockgroups in the down direction. + * + * @param group The blockgroups to connect to. + * @return True if the connection was registered, false if the connection was already registered. + */ + default boolean down(@NotNull BlockGroup... group) { + return towards(Direction.DOWN, group); + } + + /** + * Creates a new connection builder with the direction set to the down direction. + * + * @return The new connection builder. + */ + default @NotNull ConnectionBuilder down() { + return towards(Direction.DOWN); + } + + /** + * Register a connection from this blockgroup to the given blockgroups in the north direction. + * + * @param group The blockgroups to connect to. + * @return True if the connection was registered, false if the connection was already registered. + */ + default boolean back(@NotNull BlockGroup... group) { + return towards(Direction.NORTH, group); + } + + /** + * Creates a new connection builder with the direction set to the north direction. + * + * @return The new connection builder. + */ + default @NotNull ConnectionBuilder back() { + return towards(Direction.NORTH); + } + + /** + * Register a connection from this blockgroup to the given blockgroups in the east direction. + * + * @param group The blockgroups to connect to. + * @return True if the connection was registered, false if the connection was already registered. + */ + default boolean right(@NotNull BlockGroup... group) { + return towards(Direction.EAST, group); + } + + /** + * Creates a new connection builder with the direction set to the east direction. + * + * @return The new connection builder. + */ + default @NotNull ConnectionBuilder right() { + return towards(Direction.EAST); + } + + /** + * Register a connection from this blockgroup to the given blockgroups in the south direction. + * + * @param group The blockgroups to connect to. + * @return True if the connection was registered, false if the connection was already registered. + */ + default boolean front(@NotNull BlockGroup... group) { + return towards(Direction.SOUTH, group); + } + + /** + * Creates a new connection builder with the direction set to the south direction. + * + * @return The new connection builder. + */ + default @NotNull ConnectionBuilder front() { + return towards(Direction.SOUTH); + } + + /** + * Register a connection from this blockgroup to the given blockgroups in the west direction. + * + * @param group The blockgroups to connect to. + * @return True if the connection was registered, false if the connection was already registered. + */ + default boolean left(@NotNull BlockGroup... group) { + return towards(Direction.WEST, group); + } + + /** + * Creates a new connection builder with the direction set to the west direction. + * + * @return The new connection builder. + */ + default @NotNull ConnectionBuilder left() { + return towards(Direction.WEST); + } + + /** + * Register a connection from this blockgroup to the given blockgroups in the up direction. + * + * @param group The blockgroups to connect to. + * @return True if the connection was registered, false if the connection was already registered. + */ + default boolean top(@NotNull BlockGroup... group) { + return towards(Direction.UP, group); + } + + /** + * Creates a new connection builder with the direction set to the up direction. + * + * @return The new connection builder. + */ + default @NotNull ConnectionBuilder top() { + return towards(Direction.UP); + } + + /** + * Register a connection from this blockgroup to the given blockgroups in the down direction. + * + * @param group The blockgroups to connect to. + * @return True if the connection was registered, false if the connection was already registered. + */ + default boolean bottom(@NotNull BlockGroup... group) { + return towards(Direction.DOWN, group); + } + + /** + * Creates a new connection builder with the direction set to the down direction. + * + * @return The new connection builder. + */ + default @NotNull ConnectionBuilder bottom() { + return towards(Direction.DOWN); + } + + /** + * Register a connection from this blockgroup to the given blockgroups in the north, east, south, and west directions. + * + * @return True if the connection was registered, false if the connection was already registered. + */ + default boolean horizontal(@NotNull BlockGroup... group) { + boolean result = north(group); + result |= east(group); + result |= south(group); + result |= west(group); + return result; + } + + /** + * Creates a new connection builder with the directions set to the north, east, south, and west directions. + * + * @return The new connection builder. + */ + default @NotNull ConnectionBuilder horizontal() { + return north().east().south().west(); + } + + class ConnectionBuilder { + private final Set directions = new HashSet<>(); + private final ConnectionRegistry registry; + + private ConnectionBuilder(@NotNull ConnectionRegistry registry) { + this.registry = registry; + } + + /** + * Adds the given direction to the list of directions to connect to. + * + * @param direction The direction to connect to. + * @return This builder. + */ + public @NotNull ConnectionBuilder direction(@NotNull Direction direction) { + directions.add(direction); + return this; + } + + /** + * Adds the north direction to the list of directions to connect to. + * + * @return This builder. + */ + public @NotNull ConnectionBuilder north() { + return direction(Direction.NORTH); + } + + /** + * Adds the east direction to the list of directions to connect to. + * + * @return This builder. + */ + public @NotNull ConnectionBuilder east() { + return direction(Direction.EAST); + } + + /** + * Adds the south direction to the list of directions to connect to. + * + * @return This builder. + */ + public @NotNull ConnectionBuilder south() { + return direction(Direction.SOUTH); + } + + /** + * Adds the west direction to the list of directions to connect to. + * + * @return This builder. + */ + public @NotNull ConnectionBuilder west() { + return direction(Direction.WEST); + } + + /** + * Adds the up direction to the list of directions to connect to. + * + * @return This builder. + */ + public @NotNull ConnectionBuilder up() { + return direction(Direction.UP); + } + + /** + * Adds the down direction to the list of directions to connect to. + * + * @return This builder. + */ + public @NotNull ConnectionBuilder down() { + return direction(Direction.DOWN); + } + + /** + * Adds the north direction to the list of directions to connect to. + * + * @return This builder. + */ + public @NotNull ConnectionBuilder back() { + return north(); + } + + /** + * Adds the east direction to the list of directions to connect to. + * + * @return This builder. + */ + public @NotNull ConnectionBuilder right() { + return east(); + } + + /** + * Adds the south direction to the list of directions to connect to. + * + * @return This builder. + */ + public @NotNull ConnectionBuilder front() { + return south(); + } + + /** + * Adds the west direction to the list of directions to connect to. + * + * @return This builder. + */ + public @NotNull ConnectionBuilder left() { + return west(); + } + + /** + * Adds the up direction to the list of directions to connect to. + * + * @return This builder. + */ + public @NotNull ConnectionBuilder top() { + return up(); + } + + /** + * Adds the down direction to the list of directions to connect to. + * + * @return This builder. + */ + public @NotNull ConnectionBuilder bottom() { + return down(); + } + + /** + * Applies the specified connection to the blockgroup. + * + * @return True if the connection made any changes to the existing connections. + */ + public boolean apply(BlockGroup... groups) { + boolean changed = false; + for (Direction direction : directions) { + for (BlockGroup group : groups) { + changed |= registry.towards(direction, group); + } + } + return changed; + } + } + } +} diff --git a/src/main/java/net/minestom/arena/game/procedural/ProceduralArena.java b/src/main/java/net/minestom/arena/game/procedural/ProceduralArena.java new file mode 100644 index 0000000..6db5ae2 --- /dev/null +++ b/src/main/java/net/minestom/arena/game/procedural/ProceduralArena.java @@ -0,0 +1,69 @@ +package net.minestom.arena.game.procedural; + +import net.kyori.adventure.text.Component; +import net.kyori.adventure.title.Title; +import net.minestom.arena.game.Arena; +import net.minestom.arena.group.Group; +import net.minestom.arena.utils.FullbrightDimension; +import net.minestom.server.MinecraftServer; +import net.minestom.server.coordinate.Pos; +import net.minestom.server.entity.GameMode; +import net.minestom.server.entity.Player; +import net.minestom.server.instance.InstanceContainer; +import net.minestom.server.utils.time.TimeUnit; +import org.jetbrains.annotations.NotNull; + +import java.time.Duration; +import java.util.concurrent.CompletableFuture; + +public class ProceduralArena implements Arena { + + private final Group group; + + public ProceduralArena(Group group) { + this.group = group; + } + + @Override + public @NotNull Group group() { + return group; + } + + @Override + public @NotNull CompletableFuture init() { + return CompletableFuture.runAsync(() -> { + // Show the loading arena title until the arena is loaded + group.audience().showTitle( + Title.title(Component.text("Loading arena..."), + Component.empty(), + Title.Times.times( + Duration.of(1, TimeUnit.SECOND), + Duration.of(365, TimeUnit.DAY), + Duration.ZERO + ) + ) + ); + + // Load the arena + InstanceContainer instanceContainer = MinecraftServer.getInstanceManager() + .createInstanceContainer(FullbrightDimension.INSTANCE); + instanceContainer.setGenerator(new WaveFunctionCollapseGenerator(System.currentTimeMillis())); + + // Add the players to the arena + CompletableFuture[] futures = group.members().stream() + .map(player -> player.setInstance(instanceContainer, new Pos(0, 0, 0))) + .toArray(CompletableFuture[]::new); + CompletableFuture.allOf(futures).join(); + for (Player member : group.members()) { + member.setGameMode(GameMode.SPECTATOR); + } + + // Remove the loading arena title + group.audience() + .showTitle( + Title.title(Component.empty(), Component.empty(), + Title.Times.times(Duration.of(1, TimeUnit.SECOND), + Duration.of(1, TimeUnit.SECOND), Duration.ZERO))); + }); + } +} diff --git a/src/main/java/net/minestom/arena/game/procedural/WaveFunctionCollapseGenerator.java b/src/main/java/net/minestom/arena/game/procedural/WaveFunctionCollapseGenerator.java new file mode 100644 index 0000000..91ef6bc --- /dev/null +++ b/src/main/java/net/minestom/arena/game/procedural/WaveFunctionCollapseGenerator.java @@ -0,0 +1,292 @@ +package net.minestom.arena.game.procedural; + +import de.articdive.jnoise.JNoise; +import net.minestom.server.coordinate.Point; +import net.minestom.server.coordinate.Vec; +import net.minestom.server.instance.block.Block; +import net.minestom.server.instance.generator.GenerationUnit; +import net.minestom.server.instance.generator.Generator; +import net.minestom.server.utils.Direction; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import java.util.*; + +/** + * Wave function collapse is an algorithm used to generate structures from individual pieces that have connection rules. + *

+ *

+ * You start with a single random piece, this piece is placed down. + * After this piece is placed down, the surrounding pieces are restricted by the existing piece's allowed connections. + *

+ * Find the next piece with the lowest amount of allowed connections, and place it down, apply the same process again. + * This process is repeated until there are no more pieces to place down. + */ +class WaveFunctionCollapseGenerator implements Generator { + + private final Random random; + private final JNoise noise; + + WaveFunctionCollapseGenerator(long seed) { + this.random = new Random(seed); + this.noise = JNoise.newBuilder() + .superSimplex() + .setFrequency(0.1) + .setSeed(seed) + .build(); + } + + @Override + public void generate(@NotNull GenerationUnit unit) { + // Check if unit contains 0, 0, 0 + Point start = unit.absoluteStart(); + Point end = unit.absoluteEnd(); + + if ( + start.x() <= 0 && start.y() <= 0 && start.z() <= 0 && + end.x() > 0 && end.y() > 0 && end.z() > 0 + ) { + // Success, we now generate the arena + unit.fork(this::generate); + } + } + + // This is only ran once at section 0, 0, 0 + private void generate(@NotNull Block.Setter setter) { + + // This queue will contain all the super positions and is sorted by lowest entropy first + PriorityQueue queue = new PriorityQueue<>(); + + // This map is used to find the super position for a given position + Map positions = new HashMap<>(); + + // Create the super position at the center of the arena + SuperPosition origin = new SuperPosition(new Vec(0, 0, 0), positions); + origin.states.remove(GenerationData.AIR); + queue.add(origin); + positions.put(new Vec(0, 0, 0), origin); + + while (!queue.isEmpty()) { + handleNextSuperPosition(setter, queue, positions); + } + + // Debug +// MinecraftServer.getGlobalEventHandler().addListener(PlayerHandAnimationEvent.class, event -> { +// while (true) { +// SuperPosition position = handleNextSuperPosition(event.getInstance(), queue, positions); +// if (position != null) { +// if (position.placed && position.states.size() > 0) { +// event.getPlayer().teleport(event.getPlayer().getPosition().withCoord(position.pos.mul(2))); +// break; +// } +// } +// } +// }); + } + + private SuperPosition handleNextSuperPosition(Block.Setter setter, PriorityQueue queue, + Map positions) { + SuperPosition pos = queue.poll(); + if (pos == null) { + return null; + } + handleSuperPosition(pos, setter, queue, positions); + return pos; + } + + private void handleSuperPosition(SuperPosition pos, Block.Setter setter, + PriorityQueue queue, + Map positions) { + + // Get a random state from within the super position + if (pos.states.isEmpty()) { + pos.placed = true; + } else { + GenerationData.BlockGroup group = pos.randomState(random); + { + group.place(setter, pos.pos); +// int entropy = pos.states.size(); +// Audiences.all().sendMessage(Component.text("Placed " + group + " at " + pos.pos + " with entropy " + entropy).color(NamedTextColor.GRAY)); + pos.states = Set.of(group); + pos.placed = true; + } + + // Ensure that pos has all it's neighbors + pos.neighbors.createIfNotExists(pos, queue, positions); + } + + // Update surrounding super positions + for (Direction direction : Direction.values()) { + SuperPosition neighbor = pos.neighbors.towards(direction); + if (neighbor == null) { + continue; + } + if (neighbor.updateThisStateFrom(direction.opposite())) { + queue.remove(neighbor); + queue.add(neighbor); + } + } + } + + private static class SuperPosition implements Comparable { + private static final List ALL_STATES = GenerationData.ALL_BLOCK_GROUPS.stream() + .filter(group -> group != GenerationData.AIR) + .toList(); + + private final Vec pos; + private final Neighbors neighbors; + + private Set states; + private boolean placed = false; + private final int id; + + public SuperPosition(Vec pos, Map positions) { + this.pos = pos; + this.states = new HashSet<>(ALL_STATES); + this.neighbors = new Neighbors(pos, positions); + this.id = positions.size(); + } + + @Override + public int compareTo(@NotNull WaveFunctionCollapseGenerator.SuperPosition o) { + int entropy = Integer.compare(states.size(), o.states.size()); + + if (entropy != 0) { + return entropy; + } + + double distance = Double.compare(pos.distanceSquared(0, 0, 0), o.pos.distanceSquared(0, 0, 0)); + + if (distance != 0) { + return (int) Math.signum(distance); + } + + return Integer.compare(id, o.id); + } + + public @NotNull GenerationData.BlockGroup randomState(Random random) { + if (states.isEmpty()) { + return GenerationData.AIR; + } + // Get a random state + int i = random.nextInt(states.size()); + int current = 0; + for (GenerationData.BlockGroup state : states) { + if (current == i) { + return state; + } + current++; + } + throw new IllegalStateException("There is no state for this super position???"); + } + + /** + * Updates only this super position's state from surrounding neighbor's state + */ + public boolean updateThisState() { + if (placed) { + return false; + } + boolean changed = false; + for (Direction direction : Direction.values()) { + changed |= updateThisStateFrom(direction); + } + return changed; + } + + public Set allowedInDirection(Direction direction) { + Set out = new HashSet<>(ALL_STATES); + for (GenerationData.BlockGroup state : states) { + out.retainAll(state.allowsTo(direction)); + } + return out; + } + + public boolean updateThisStateFrom(@NotNull Direction direction) { + if (placed) { + return false; + } + SuperPosition neighbor = neighbors.towards(direction); + if (neighbor == null) { + return false; + } + + direction = direction.opposite(); + var allowed = neighbor.allowedInDirection(direction); +// int entropy = states.size(); + boolean retainChanged = this.states.retainAll(allowed); + +// if (retainChanged) { +// String coord = String.format("Updated (%s, %s, %s): ", pos.x(), pos.y(), pos.z()); +// String info = entropy + " -> " + states.size(); +// Audiences.all().sendMessage(Component.text(coord + info).color(NamedTextColor.DARK_GRAY)); +// } + + return retainChanged; + } + + public static class Neighbors implements Iterable { + private final Vec pos; + private final Map positions; + + public Neighbors(Vec pos, Map positions) { + this.pos = pos; + this.positions = positions; + } + + public Map all() { + Map out = new HashMap<>(); + for (Direction direction : Direction.values()) { + out.put(direction, towards(direction)); + } + return out; + } + + public @Nullable SuperPosition towards(Direction direction) { + return positions.get(pos.add(direction.normalX(), direction.normalY(), direction.normalZ())); + } + + public void createIfNotExists(SuperPosition pos, PriorityQueue queue, + Map positions) { + for (Direction direction : Direction.values()) { + if (towards(direction) == null) { + int x = pos.pos.blockX() + direction.normalX(); + int y = pos.pos.blockY() + direction.normalY(); + int z = pos.pos.blockZ() + direction.normalZ(); + if (Math.sqrt(x * x + y * y + z * z) > GenerationData.MAX_DISTANCE) { + continue; + } + + // Find a neighbor in the map + Vec newVec = new Vec(x, y, z); + + positions.computeIfAbsent(newVec, ignored -> { + SuperPosition newPos = new SuperPosition(newVec, positions); + queue.add(newPos); + return newPos; + }); + } + } + } + + @NotNull + @Override + public Iterator iterator() { + Direction[] dirs = Direction.values(); + int[] i = {0}; + + return new Iterator<>() { + @Override + public boolean hasNext() { + return i[0] < dirs.length; + } + + @Override + public @Nullable SuperPosition next() { + return towards(dirs[i[0]++]); + } + }; + } + } + } +} diff --git a/src/main/java/net/minestom/arena/group/Group.java b/src/main/java/net/minestom/arena/group/Group.java index 57dff7c..04d2673 100644 --- a/src/main/java/net/minestom/arena/group/Group.java +++ b/src/main/java/net/minestom/arena/group/Group.java @@ -12,6 +12,8 @@ static Group findGroup(@NotNull Player player) { } @NotNull Player leader(); + @NotNull Set<@NotNull Player> members(); + @NotNull Audience audience(); } diff --git a/src/main/java/net/minestom/arena/group/GroupCommand.java b/src/main/java/net/minestom/arena/group/GroupCommand.java index 00f72c7..fa8d060 100644 --- a/src/main/java/net/minestom/arena/group/GroupCommand.java +++ b/src/main/java/net/minestom/arena/group/GroupCommand.java @@ -5,8 +5,8 @@ import net.kyori.adventure.text.event.ClickEvent; import net.kyori.adventure.text.event.HoverEvent; import net.kyori.adventure.text.format.NamedTextColor; -import net.minestom.arena.utils.CommandUtils; import net.minestom.arena.Messenger; +import net.minestom.arena.utils.CommandUtils; import net.minestom.server.command.builder.Command; import net.minestom.server.entity.Player; import net.minestom.server.utils.entity.EntityFinder; From c8285913f9af8dabcdf6c1fe5d4021f14952ae4f Mon Sep 17 00:00:00 2001 From: KrystilizeNevaDies Date: Sun, 1 May 2022 23:56:11 +1000 Subject: [PATCH 2/2] Move ArenaType to record --- .../net/minestom/arena/game/ArenaCommand.java | 54 ++++++++----------- .../arena/game/procedural/GenerationData.java | 17 ++---- 2 files changed, 27 insertions(+), 44 deletions(-) diff --git a/src/main/java/net/minestom/arena/game/ArenaCommand.java b/src/main/java/net/minestom/arena/game/ArenaCommand.java index 7863c9e..4cfbb3c 100644 --- a/src/main/java/net/minestom/arena/game/ArenaCommand.java +++ b/src/main/java/net/minestom/arena/game/ArenaCommand.java @@ -21,34 +21,26 @@ import org.jetbrains.annotations.NotNull; import java.util.List; +import java.util.Map; import java.util.function.Consumer; import java.util.function.Function; public final class ArenaCommand extends Command { - // In order to add a new arena, add it to this enum. - private enum ArenaType { - MOB(Material.ZOMBIE_HEAD, - builder -> builder.displayName(Component.text("Mob Arena", NamedTextColor.GREEN)), - MobArena::new), - PROCEDURAL(Material.BONE_BLOCK, - builder -> builder.displayName(Component.text("Procedural", NamedTextColor.RED)), - ProceduralArena::new); - - private final ItemStack menuItem; - private final Function arenaCreator; - - ArenaType(Material material, Consumer builderConsumer, - Function arenaCreator) { - this.arenaCreator = arenaCreator; - ItemStack.Builder builder = ItemStack.builder(material); - builderConsumer.accept(builder); - this.menuItem = builder.build(); - } - - public @NotNull ItemStack menuItem() { - return menuItem; - } + // In order to add a new arena, add it to this section: + public static final ArenaType MOB = new ArenaType("mob", ItemStack.of(Material.ZOMBIE_HEAD), + builder -> builder.displayName(Component.text("Mob Arena", NamedTextColor.GREEN)), + MobArena::new); + public static final ArenaType PROCEDURAL = new ArenaType("procedural", ItemStack.of(Material.BONE_BLOCK), + builder -> builder.displayName(Component.text("Procedural", NamedTextColor.RED)), + ProceduralArena::new); + public static final Map ARENAS = Map.of( + "mob", MOB, + "procedural", PROCEDURAL + ); + + private record ArenaType(String name, ItemStack menuItem, Consumer builderConsumer, + Function arenaCreator) { public Arena startNew(Group group) { return arenaCreator.apply(group); @@ -64,11 +56,11 @@ public ArenaCommand() { addSyntax( (sender, context) -> play((Player) sender, context.get("type")), - ArgumentType.Enum("type", ArenaType.class) + ArgumentType.Word("type").from(ARENAS.keySet().toArray(String[]::new)) ); } - private static void play(Player player, ArenaType type) { + private static void play(Player player, String type) { if (player.getInstance() != Lobby.INSTANCE) { Messenger.warn(player, "You are not in the lobby! Join the lobby first."); return; @@ -78,12 +70,12 @@ private static void play(Player player, ArenaType type) { Messenger.warn(player, "You are not the leader of your group!"); return; } - Arena arena = type.startNew(group); + Arena arena = ARENAS.get(type).startNew(group); arena.init().thenRun(() -> group.members().forEach(Player::refreshCommands)); } private static class ArenaInventory extends Inventory { - private static final Tag ARENA_TAG = Tag.String("arena").map(ArenaType::valueOf, ArenaType::name); + private static final Tag ARENA_TAG = Tag.String("arena"); private static final ItemStack HEADER = ItemUtils.stripItalics(ItemStack.builder(Material.IRON_BARS) .displayName(Component.text("Arena", NamedTextColor.RED)) .lore(Component.text("Select an arena to play in", NamedTextColor.GRAY)) @@ -95,14 +87,14 @@ public ArenaInventory() { setItemStack(4, HEADER); setItemStack(31, Items.CLOSE); - int i = 13 - ArenaType.values().length / 2; - for (ArenaType type : ArenaType.values()) { + int i = 13 - ARENAS.size() / 2; + for (ArenaType type : ARENAS.values()) { ItemStack item = type.menuItem(); setItemStack( i++, ItemUtils.stripItalics( item.withLore(List.of(Component.text("Click to play in the " + type.name() + " arena", NamedTextColor.GRAY))) - .withTag(ARENA_TAG, type) + .withTag(ARENA_TAG, type.name()) ) ); } @@ -115,7 +107,7 @@ public ArenaInventory() { return; } - ArenaType arena = result.getClickedItem().getTag(ARENA_TAG); + String arena = result.getClickedItem().getTag(ARENA_TAG); if (arena != null) { player.closeInventory(); diff --git a/src/main/java/net/minestom/arena/game/procedural/GenerationData.java b/src/main/java/net/minestom/arena/game/procedural/GenerationData.java index 238f758..caa1a95 100644 --- a/src/main/java/net/minestom/arena/game/procedural/GenerationData.java +++ b/src/main/java/net/minestom/arena/game/procedural/GenerationData.java @@ -5,6 +5,7 @@ import net.minestom.server.utils.Direction; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; +import org.jetbrains.annotations.UnknownNullability; import java.util.*; import java.util.function.Consumer; @@ -62,12 +63,6 @@ private enum BlockGroups implements BlockGroup { null, null, null, null ), - BLANK( - null, null, - null, null, - null, null, - null, null - ), COBBLE_PATH_LEFT( null, null, COBBLESTONE, COBBLESTONE, @@ -92,12 +87,8 @@ private enum BlockGroups implements BlockGroup { private static final Map group2Connections = new HashMap<>(); static { - // AIR connects to everything - connections(AIR, towards -> towards.north().east().south().west().up().down().apply(BlockGroups.values())); - - // Blank - connections(BLANK, towards -> { - }); + // AIR connects to nothing + connections(AIR, towards -> {}); // Cobble paths connections(COBBLE_PATH_LEFT, towards -> { @@ -124,7 +115,7 @@ private enum BlockGroups implements BlockGroup { private final Block block5; private final Block block6; private final Block block7; - private @Nullable Connections connections; + private @UnknownNullability Connections connections; BlockGroups(Block block0, Block block1, Block block2, Block block3, Block block4, Block block5, Block block6, Block block7) {