diff --git a/.github/workflows/build-native.yaml b/.github/workflows/build-native.yaml index 4c0b601..7427dfa 100644 --- a/.github/workflows/build-native.yaml +++ b/.github/workflows/build-native.yaml @@ -11,6 +11,7 @@ on: - 'blocksumo/**' - 'battle/**' - 'minesweeper/**' + - 'holeymoley/**' - 'parkourtag/**' - 'lazertag/**' - 'lobby/**' diff --git a/battle/build.gradle.kts b/battle/build.gradle.kts index fe99b0b..86e4085 100644 --- a/battle/build.gradle.kts +++ b/battle/build.gradle.kts @@ -13,7 +13,7 @@ repositories { dependencies { implementation(project(":core")) - implementation("com.github.vibenilla:pvp:133ae66") { + implementation("com.github.vibenilla:pvp:d5ecbaf") { exclude(group = "net.minestom", module = "minestom") } } diff --git a/battle/src/main/java/dev/emortal/minestom/battle/game/PlayerTeams.java b/battle/src/main/java/dev/emortal/minestom/battle/game/PlayerTeams.java index 05db5d4..928ddd6 100644 --- a/battle/src/main/java/dev/emortal/minestom/battle/game/PlayerTeams.java +++ b/battle/src/main/java/dev/emortal/minestom/battle/game/PlayerTeams.java @@ -9,12 +9,12 @@ import org.jetbrains.annotations.NotNull; public final class PlayerTeams { - public static final @NotNull Team ALIVE = MinecraftServer.getTeamManager().createBuilder("alive") + public static final @NotNull Team ALIVE = MinecraftServer.getTeamManager().createBuilder("battle_alive") .teamColor(TeamColor.RED) .nameTagVisibility(TeamsPacket.NameTagVisibility.ALWAYS) .updateTeamPacket() .build(); - public static final @NotNull Team DEAD = MinecraftServer.getTeamManager().createBuilder("dead") + public static final @NotNull Team DEAD = MinecraftServer.getTeamManager().createBuilder("battle_dead") .teamColor(TeamColor.GRAY) .prefix(Component.text("☠ ", NamedTextColor.GRAY)) .nameTagVisibility(TeamsPacket.NameTagVisibility.NEVER) diff --git a/fatserver/build.gradle.kts b/fatserver/build.gradle.kts index 768653c..ca186fd 100644 --- a/fatserver/build.gradle.kts +++ b/fatserver/build.gradle.kts @@ -21,6 +21,7 @@ dependencies { implementation(project(":blocksumo")) implementation(project(":lazertag")) implementation(project(":minesweeper")) + implementation(project(":holeymoley")) } java { diff --git a/fatserver/src/main/java/dev/emortal/fatserver/Main.java b/fatserver/src/main/java/dev/emortal/fatserver/Main.java index aaa752e..b3459e4 100644 --- a/fatserver/src/main/java/dev/emortal/fatserver/Main.java +++ b/fatserver/src/main/java/dev/emortal/fatserver/Main.java @@ -3,6 +3,7 @@ import dev.emortal.minestom.battle.BattleModule; import dev.emortal.minestom.blocksumo.BlockSumoModule; import dev.emortal.minestom.core.EmortalServer; +import dev.emortal.minestom.holeymoley.HoleyMoleyModule; import dev.emortal.minestom.marathon.MarathonModule; import dev.emortal.minestom.minesweeper.MinesweeperModule; import dev.emortal.minestom.parkourtag.ParkourTagModule; @@ -15,7 +16,8 @@ void main() { new BattleModule(), new BlockSumoModule(), // new LazerTagModule(), Has 60 TPS so cannot be used in fat server - new MinesweeperModule() + new MinesweeperModule(), + new HoleyMoleyModule() ); } } diff --git a/holeymoley/Dockerfile b/holeymoley/Dockerfile new file mode 100644 index 0000000..06fa01b --- /dev/null +++ b/holeymoley/Dockerfile @@ -0,0 +1,9 @@ +FROM eclipse-temurin:25-jre-alpine + +RUN mkdir /app +WORKDIR /app + +COPY build/libs/*-all.jar /app/holeymoley.jar + +ENTRYPOINT ["java"] +CMD ["-jar", "/app/holeymoley.jar"] diff --git a/holeymoley/build.gradle.kts b/holeymoley/build.gradle.kts new file mode 100644 index 0000000..5bc1d54 --- /dev/null +++ b/holeymoley/build.gradle.kts @@ -0,0 +1,43 @@ +plugins { + java + id("com.gradleup.shadow") version "9.5.1" +} + +group = "dev.emortal.minestom.holeymoley" +version = "1.0-SNAPSHOT" + +repositories { + maven("https://jitpack.io") +} + +dependencies { + implementation(project(":core")) + + implementation("com.github.vibenilla:pvp:d5ecbaf") { + exclude(group = "net.minestom", module = "minestom") + } +} + +java { + toolchain { + languageVersion.set(JavaLanguageVersion.of(25)) + } +} + +tasks { + shadowJar { + duplicatesStrategy = DuplicatesStrategy.INCLUDE + mergeServiceFiles() + + manifest { + attributes( + "Main-Class" to "dev.emortal.minestom.holeymoley.Main", + "Multi-Release" to true + ) + } + } + + build { + dependsOn(shadowJar) + } +} diff --git a/holeymoley/src/main/java/dev/emortal/minestom/holeymoley/HoleyMoleyModule.java b/holeymoley/src/main/java/dev/emortal/minestom/holeymoley/HoleyMoleyModule.java new file mode 100644 index 0000000..ad1fe8f --- /dev/null +++ b/holeymoley/src/main/java/dev/emortal/minestom/holeymoley/HoleyMoleyModule.java @@ -0,0 +1,42 @@ +package dev.emortal.minestom.holeymoley; + +import dev.emortal.minestom.core.Module; +import dev.emortal.minestom.core.game.GameCreator; +import dev.emortal.minestom.core.map.MapManager; +import dev.emortal.minestom.holeymoley.game.HoleyMoleyGame; +import dev.emortal.minestom.holeymoley.map.HoleyMoleyMapManager; +import io.github.togar2.pvp.MinestomPvP; + +public class HoleyMoleyModule implements Module { + public static final int MAP_RADIUS = 25; + + @Override + public String getId() { + return "holeymoley"; + } + + @Override + public int getMinPlayers() { + return 2; + } + + @Override + public int getMaxPlayers() { + return 20; + } + + @Override + public GameCreator getGameCreator() { + return HoleyMoleyGame::new; + } + + @Override + public MapManager getMapManager() { + return new HoleyMoleyMapManager(MAP_RADIUS); + } + + @Override + public void preRegister() { + MinestomPvP.init(); + } +} diff --git a/holeymoley/src/main/java/dev/emortal/minestom/holeymoley/Main.java b/holeymoley/src/main/java/dev/emortal/minestom/holeymoley/Main.java new file mode 100644 index 0000000..8800928 --- /dev/null +++ b/holeymoley/src/main/java/dev/emortal/minestom/holeymoley/Main.java @@ -0,0 +1,9 @@ +package dev.emortal.minestom.holeymoley; + +import dev.emortal.minestom.core.EmortalServer; + +public final class Main { + void main() { + EmortalServer.start(new HoleyMoleyModule()); + } +} diff --git a/holeymoley/src/main/java/dev/emortal/minestom/holeymoley/chest/ChestBlockHandler.java b/holeymoley/src/main/java/dev/emortal/minestom/holeymoley/chest/ChestBlockHandler.java new file mode 100644 index 0000000..9bb6e11 --- /dev/null +++ b/holeymoley/src/main/java/dev/emortal/minestom/holeymoley/chest/ChestBlockHandler.java @@ -0,0 +1,59 @@ +package dev.emortal.minestom.holeymoley.chest; + +import net.kyori.adventure.key.Key; +import net.minestom.server.instance.block.Block; +import net.minestom.server.instance.block.BlockHandler; +import net.minestom.server.inventory.Inventory; +import net.minestom.server.inventory.InventoryType; +import net.minestom.server.item.ItemStack; +import org.jetbrains.annotations.NotNull; + +import java.util.concurrent.ThreadLocalRandom; +import java.util.concurrent.atomic.AtomicInteger; + +public final class ChestBlockHandler implements BlockHandler { + + private final Inventory inventory = new Inventory(InventoryType.CHEST_3_ROW, ""); + private final AtomicInteger playersInside = new AtomicInteger(0); + + public ChestBlockHandler() { + super(); + this.refillInventory(); + } + + public @NotNull Inventory getInventory() { + return this.inventory; + } + + public int addPlayerInside() { + return this.playersInside.incrementAndGet(); + } + + public int removePlayerInside() { + return this.playersInside.decrementAndGet(); + } + + @Override + public @NotNull Key getKey() { + return Block.CHEST.key(); + } + + public void refillInventory() { + this.inventory.clear(); + for (int i = 0; i < 7; i++) { + addRandomly(this.inventory, Items.random()); + } + } + + private static void addRandomly(@NotNull Inventory inventory, @NotNull ItemStack itemStack) { + ThreadLocalRandom random = ThreadLocalRandom.current(); + + while (true) { + int randomSlot = random.nextInt(inventory.getSize()); + if (inventory.getItemStack(randomSlot) == ItemStack.AIR) { + inventory.setItemStack(randomSlot, itemStack); + break; + } + } + } +} diff --git a/holeymoley/src/main/java/dev/emortal/minestom/holeymoley/chest/ChestUpdateHandler.java b/holeymoley/src/main/java/dev/emortal/minestom/holeymoley/chest/ChestUpdateHandler.java new file mode 100644 index 0000000..b0e0e70 --- /dev/null +++ b/holeymoley/src/main/java/dev/emortal/minestom/holeymoley/chest/ChestUpdateHandler.java @@ -0,0 +1,233 @@ +package dev.emortal.minestom.holeymoley.chest; + +import dev.emortal.minestom.holeymoley.game.HoleyMoleyGame; +import net.kyori.adventure.key.Key; +import net.kyori.adventure.sound.Sound; +import net.kyori.adventure.text.Component; +import net.kyori.adventure.text.format.NamedTextColor; +import net.kyori.adventure.title.Title; +import net.minestom.server.coordinate.Point; +import net.minestom.server.entity.GameMode; +import net.minestom.server.entity.Player; +import net.minestom.server.event.inventory.InventoryCloseEvent; +import net.minestom.server.event.player.PlayerBlockInteractEvent; +import net.minestom.server.instance.Instance; +import net.minestom.server.instance.block.Block; +import net.minestom.server.network.packet.server.play.BlockActionPacket; +import net.minestom.server.network.packet.server.play.ParticlePacket; +import net.minestom.server.particle.Particle; +import net.minestom.server.sound.SoundEvent; +import net.minestom.server.timer.TaskSchedule; +import org.jetbrains.annotations.NotNull; + +import java.time.Duration; +import java.util.*; +import java.util.function.Supplier; + +public final class ChestUpdateHandler { + private static final TaskSchedule CHEST_REFILL_INTERVAL = TaskSchedule.seconds(120); + + private final @NotNull HoleyMoleyGame game; + private final @NotNull Instance instance; + + private final Map chests = new HashMap<>(); + private final Set unopenedChests = new HashSet<>(); + private final Map chestsByPlayer = new HashMap<>(); + + public ChestUpdateHandler(@NotNull HoleyMoleyGame game) { + this.game = game; + this.instance = game.getInstance(); + + this.registerBackgroundTasks(); + game.getEventNode().addListener(PlayerBlockInteractEvent.class, this::onBlockInteract); + game.getEventNode().addListener(InventoryCloseEvent.class, this::onInventoryClose); + } + + private void onBlockInteract(@NotNull PlayerBlockInteractEvent event) { + if (event.getPlayer().getGameMode() == GameMode.SPECTATOR) return; + + Block block = event.getBlock(); + if (!block.compare(Block.CHEST)) return; + + Point pos = event.getBlockPosition(); + ChestBlockHandler chestHandler = this.getOrCreateChestHandler(pos); + + event.getPlayer().openInventory(chestHandler.getInventory()); + + this.unopenedChests.remove(pos); + this.chestsByPlayer.put(event.getPlayer().getUuid(), pos); + + int playersInside = chestHandler.addPlayerInside(); + this.updatePlayersInsideChest(pos, playersInside); + + if (playersInside == 1) { + // This is the first player to open the chest + this.playChestOpenSound(pos); + } + } + + private void onInventoryClose(@NotNull InventoryCloseEvent event) { + Player player = event.getPlayer(); + + Point openChestPos = this.chestsByPlayer.remove(player.getUuid()); + if (openChestPos == null) return; + + ChestBlockHandler handler = this.chests.get(openChestPos); + int playersInside = handler.removePlayerInside(); + + this.updatePlayersInsideChest(openChestPos, playersInside); + if (playersInside == 0) { + this.playChestCloseSound(openChestPos); + } + } + + public @NotNull ChestBlockHandler getOrCreateChestHandler(@NotNull Point pos) { + ChestBlockHandler handler = this.chests.get(pos); + + if (handler == null) { + ChestBlockHandler newHandler = new ChestBlockHandler(); + this.chests.put(pos, newHandler); + handler = newHandler; + } + + return handler; + } + + private void updatePlayersInsideChest(@NotNull Point chestPos, int newCount) { + // The animation of the chest opening and closing is completely controlled by the client + // all we have to do is just tell it when the players inside count changes + this.instance.sendGroupedPacket(new BlockActionPacket(chestPos, (byte) 1, (byte) newCount, Block.CHEST)); + } + + private void playChestOpenSound(@NotNull Point pos) { + this.game.playSound(Sound.sound(SoundEvent.BLOCK_CHEST_OPEN, Sound.Source.BLOCK, 1f, 1f), pos.x(), pos.y(), pos.z()); + } + + private void playChestCloseSound(@NotNull Point pos) { + this.game.playSound(Sound.sound(SoundEvent.BLOCK_CHEST_CLOSE, Sound.Source.BLOCK, 1f, 1f), pos.x(), pos.y(), pos.z()); + } + + private void registerBackgroundTasks() { + // Chest periodic refill task + this.instance.scheduler() + .buildTask(() -> { + if (this.game.hasEnded()) return; // Stop refilling chests once game has been won + this.refillChests(); + }) + .delay(CHEST_REFILL_INTERVAL) + .repeat(CHEST_REFILL_INTERVAL) + .schedule(); + + this.instance.scheduler().buildTask(new DisplayChestRefillParticlesTask()).schedule(); + } + + private void refillChests() { + this.unopenedChests.clear(); + + for (Map.Entry chestEntry : this.chests.entrySet()) { + chestEntry.getValue().refillInventory(); + this.unopenedChests.add(chestEntry.getKey()); + } + + this.animateRefillChestTitle(); + } + + private void animateRefillChestTitle() { + this.playRefillOpenSound(); + this.instance.scheduler().submitTask(new AnimateChestTask(this.game, this.instance)); + } + + private void playRefillOpenSound() { + this.game.playSound(Sound.sound(Key.key("battle.refill.open"), Sound.Source.MASTER, 1f, 1f), Sound.Emitter.self()); + } + + private final class DisplayChestRefillParticlesTask implements Runnable { + + private double i = 0.0; + + @Override + public void run() { + if (this.i > 2 * Math.PI) { + this.i = this.i % 2 * Math.PI; + } + + for (Point unopenedChest : ChestUpdateHandler.this.unopenedChests) { + ParticlePacket packet = this.createParticlePacket(unopenedChest); + ChestUpdateHandler.this.game.sendGroupedPacket(packet); + } + } + + private @NotNull ParticlePacket createParticlePacket(@NotNull Point pos) { + return new ParticlePacket(Particle.DUST.withColor(NamedTextColor.YELLOW).withScale(0.75f), pos.x(), pos.y(), pos.z(), 0F, 0F, 0F, 0, 1); + } + } + + private static final class AnimateChestTask implements Supplier { + private static final Component[] FRAMES = new Component[] {Component.text('\uE00E'), Component.text('\uE00F'), Component.text('\uE010')}; + + private final @NotNull HoleyMoleyGame game; + private final @NotNull Instance instance; + + private int frame = 0; + + AnimateChestTask(@NotNull HoleyMoleyGame game, @NotNull Instance instance) { + this.game = game; + this.instance = instance; + } + + @Override + public @NotNull TaskSchedule get() { + if (!this.showedAllFrames()) { + this.showNextFrame(); + this.frame++; + return TaskSchedule.tick(2); + } + + this.instance.scheduler().submitTask(new Supplier<>() { + boolean firstTick = true; + boolean playedSound = false; + + @Override + public TaskSchedule get() { + if (this.firstTick) { + this.firstTick = false; + return TaskSchedule.seconds(1); + } + + if (!this.playedSound) { + this.playedSound = true; + AnimateChestTask.this.playRefillCloseSound(); + } + + AnimateChestTask.this.frame--; + AnimateChestTask.this.showNextFrame(); + + if (AnimateChestTask.this.frame == 0) { + return TaskSchedule.stop(); + } + + return TaskSchedule.tick(2); + } + }); + + return TaskSchedule.stop(); + } + + private boolean showedAllFrames() { + return this.frame >= FRAMES.length; + } + + private void showNextFrame() { + Component nextFrame = FRAMES[this.frame]; + this.game.showTitle(Title.title( + nextFrame, + Component.empty(), + Title.Times.times(Duration.ZERO, Duration.ofMillis(1200), Duration.ofMillis(200)) + )); + } + + private void playRefillCloseSound() { + this.game.playSound(Sound.sound(Key.key("battle.refill.close"), Sound.Source.MASTER, 1f, 1f), Sound.Emitter.self()); + } + } +} diff --git a/holeymoley/src/main/java/dev/emortal/minestom/holeymoley/chest/Item.java b/holeymoley/src/main/java/dev/emortal/minestom/holeymoley/chest/Item.java new file mode 100644 index 0000000..b9a2237 --- /dev/null +++ b/holeymoley/src/main/java/dev/emortal/minestom/holeymoley/chest/Item.java @@ -0,0 +1,51 @@ +package dev.emortal.minestom.holeymoley.chest; + +import net.minestom.server.item.ItemStack; +import net.minestom.server.item.Material; +import org.jetbrains.annotations.NotNull; + +import java.util.concurrent.ThreadLocalRandom; +import java.util.function.Consumer; + +public final class Item { + + private final @NotNull Material material; + private final int weight; + private final @NotNull Consumer itemCreate; + private final @NotNull ItemStack itemStack; + + public Item(@NotNull Material material, int weight, @NotNull Consumer itemCreate) { + this.material = material; + this.weight = weight; + this.itemCreate = itemCreate; + this.itemStack = this.createItemStack(); + } + + public Item(@NotNull Material material, int weight) { + this(material, weight, builder -> {}); + } + + public Item(@NotNull Material material, int weight, int minAmount, int maxAmount) { + this(material, weight, builder -> builder.amount(randomAmount(minAmount, maxAmount))); + } + + private static int randomAmount(int minAmount, int maxAmount) { + if (minAmount >= maxAmount) return minAmount; + return ThreadLocalRandom.current().nextInt(minAmount, maxAmount + 1); + } + + private @NotNull ItemStack createItemStack() { + ItemStack.Builder builder = ItemStack.builder(this.material); + this.itemCreate.accept(builder); + + return builder.build(); + } + + public @NotNull ItemStack getItemStack() { + return this.itemStack; + } + + public int getWeight() { + return weight; + } +} diff --git a/holeymoley/src/main/java/dev/emortal/minestom/holeymoley/chest/Items.java b/holeymoley/src/main/java/dev/emortal/minestom/holeymoley/chest/Items.java new file mode 100644 index 0000000..0fc4deb --- /dev/null +++ b/holeymoley/src/main/java/dev/emortal/minestom/holeymoley/chest/Items.java @@ -0,0 +1,143 @@ +package dev.emortal.minestom.holeymoley.chest; + +import net.kyori.adventure.text.Component; +import net.kyori.adventure.text.format.NamedTextColor; +import net.kyori.adventure.text.format.TextDecoration; +import net.minestom.server.component.DataComponents; +import net.minestom.server.item.ItemStack; +import net.minestom.server.item.Material; +import net.minestom.server.item.component.EnchantmentList; +import net.minestom.server.item.component.PotionContents; +import net.minestom.server.item.enchant.Enchantment; +import net.minestom.server.potion.CustomPotionEffect; +import net.minestom.server.potion.PotionEffect; +import net.minestom.server.potion.PotionType; +import org.jetbrains.annotations.NotNull; + +import java.util.List; +import java.util.Map; +import java.util.concurrent.ThreadLocalRandom; +import java.util.function.Consumer; + +public final class Items { + + private static final int COMMON = 40; + private static final int LESS_COMMON = 35; + private static final int UNCOMMON = 30; + private static final int RARE = 25; + private static final int VERY_RARE = 15; + private static final int LEGENDARY = 1; + + private static final @NotNull List ALL = List.of( + // Consumables + new Item(Material.APPLE, COMMON), + new Item(Material.APPLE, COMMON), + new Item(Material.COOKED_BEEF, COMMON), + new Item(Material.COOKED_PORKCHOP, COMMON), + new Item(Material.COOKED_PORKCHOP, COMMON), + new Item(Material.GOLDEN_APPLE, VERY_RARE), + + // Blocks + new Item(Material.TNT, RARE, 1, 3), + new Item(Material.COBWEB, RARE, 2, 5), + + // Swords + new Item(Material.STONE_SWORD, UNCOMMON), + new Item(Material.IRON_SWORD, VERY_RARE), + new Item(Material.DIAMOND_SWORD, LEGENDARY), + + // Axes + new Item(Material.WOODEN_AXE, RARE), + new Item(Material.STONE_AXE, LEGENDARY), + + // Pickaxes + new Item(Material.WOODEN_PICKAXE, COMMON), + new Item(Material.GOLDEN_PICKAXE, LESS_COMMON), + new Item(Material.STONE_PICKAXE, UNCOMMON), + new Item(Material.IRON_PICKAXE, RARE), + new Item(Material.DIAMOND_PICKAXE, VERY_RARE), + + // Misc + new Item(Material.WOODEN_HOE, RARE, (it) -> it.set(DataComponents.ENCHANTMENTS, new EnchantmentList(Map.of(Enchantment.FIRE_ASPECT, 1)))), + new Item(Material.DIAMOND_HOE, LEGENDARY), + + // Potions + new Item(Material.POTION, RARE, (it) -> it.set(DataComponents.POTION_CONTENTS, new PotionContents(PotionType.STRONG_HEALING))), + new Item(Material.POTION, RARE, (it) -> it.set(DataComponents.POTION_CONTENTS, new PotionContents(PotionType.FIRE_RESISTANCE))), + new Item(Material.POTION, RARE, potionHelper(PotionEffect.INVISIBILITY, 20)), + new Item(Material.POTION, VERY_RARE, potionHelper(PotionEffect.STRENGTH, 20)), + new Item(Material.SPLASH_POTION, RARE, (it) -> it.set(DataComponents.POTION_CONTENTS, new PotionContents(PotionType.REGENERATION))), + new Item(Material.SPLASH_POTION, RARE, (it) -> it.set(DataComponents.POTION_CONTENTS, new PotionContents(PotionType.STRONG_HARMING))), + new Item(Material.SPLASH_POTION, RARE, potionHelper(PotionEffect.POISON, 16)), + new Item(Material.SPLASH_POTION, RARE, potionHelper(PotionEffect.SLOWNESS, 40)), + new Item(Material.SPLASH_POTION, RARE, potionHelper(PotionEffect.WEAKNESS, 45)), + + // Armour + // Helmets + new Item(Material.LEATHER_HELMET, COMMON), + new Item(Material.GOLDEN_HELMET, UNCOMMON), + new Item(Material.IRON_HELMET, VERY_RARE), + new Item(Material.DIAMOND_HELMET, LEGENDARY), + + // Chestplates + new Item(Material.LEATHER_CHESTPLATE, COMMON), + new Item(Material.CHAINMAIL_CHESTPLATE, UNCOMMON), + new Item(Material.IRON_CHESTPLATE, VERY_RARE), + new Item(Material.DIAMOND_CHESTPLATE, LEGENDARY), + + // Leggings + new Item(Material.LEATHER_LEGGINGS, COMMON), + new Item(Material.CHAINMAIL_LEGGINGS, UNCOMMON), + new Item(Material.IRON_LEGGINGS, VERY_RARE), + new Item(Material.DIAMOND_LEGGINGS, LEGENDARY), + + // Boots + new Item(Material.LEATHER_BOOTS, COMMON), + new Item(Material.CHAINMAIL_BOOTS, UNCOMMON), + new Item(Material.IRON_BOOTS, VERY_RARE), + new Item(Material.DIAMOND_BOOTS, LEGENDARY) + ); + + private Items() { + } + + public static @NotNull ItemStack random() { + int totalWeight = 0; + for (Item item : ALL) { + totalWeight += item.getWeight(); + } + + int currentIndex = 0; + int randomIndex = ThreadLocalRandom.current().nextInt(totalWeight + 1); + + while (currentIndex < ALL.size() - 1) { + randomIndex -= ALL.get(currentIndex).getWeight(); + if (randomIndex <= 0.0) break; + ++currentIndex; + } + + return ALL.get(currentIndex).getItemStack(); + } + + private static @NotNull Consumer potionHelper(@NotNull PotionEffect potionEffect, int secondsDuration) { + String potionName = titleCase(potionEffect.key().value().replace('_', ' ')); + Component potionDisplayName = Component.text("Potion of " + potionName, NamedTextColor.WHITE).decoration(TextDecoration.ITALIC, false); + + return builder -> builder + .set(DataComponents.POTION_CONTENTS, new PotionContents(List.of(new CustomPotionEffect(potionEffect, (byte) 0, secondsDuration * 20, false, true, true)))) + .customName(potionDisplayName); + } + + private static @NotNull String titleCase(@NotNull String input) { // fire resistance -> Fire Resistance + char[] chars = input.toCharArray(); + + for (int i = 0; i < chars.length; i++) { + chars[0] = Character.toUpperCase(chars[0]); + if (chars[i] == ' ') { + chars[i + 1] = Character.toUpperCase(chars[i + 1]); + } + } + + return String.valueOf(chars); + } +} diff --git a/holeymoley/src/main/java/dev/emortal/minestom/holeymoley/event/BlindnessEvent.java b/holeymoley/src/main/java/dev/emortal/minestom/holeymoley/event/BlindnessEvent.java new file mode 100644 index 0000000..d79fd95 --- /dev/null +++ b/holeymoley/src/main/java/dev/emortal/minestom/holeymoley/event/BlindnessEvent.java @@ -0,0 +1,20 @@ +package dev.emortal.minestom.holeymoley.event; + +import dev.emortal.minestom.holeymoley.game.HoleyMoleyGame; +import net.minestom.server.entity.Player; +import net.minestom.server.potion.Potion; +import net.minestom.server.potion.PotionEffect; + +public class BlindnessEvent extends Event { + + public BlindnessEvent() { + super("Blindness"); + } + + @Override + public void doEvent(HoleyMoleyGame game) { + for (Player player : game.getAlivePlayers()) { + player.addEffect(new Potion(PotionEffect.BLINDNESS, 0, 10 * 20)); + } + } +} diff --git a/holeymoley/src/main/java/dev/emortal/minestom/holeymoley/event/CloseCallEvent.java b/holeymoley/src/main/java/dev/emortal/minestom/holeymoley/event/CloseCallEvent.java new file mode 100644 index 0000000..19f10e6 --- /dev/null +++ b/holeymoley/src/main/java/dev/emortal/minestom/holeymoley/event/CloseCallEvent.java @@ -0,0 +1,21 @@ +package dev.emortal.minestom.holeymoley.event; + +import dev.emortal.minestom.holeymoley.game.HoleyMoleyGame; +import net.minestom.server.entity.Player; +import net.minestom.server.potion.Potion; +import net.minestom.server.potion.PotionEffect; + +public class CloseCallEvent extends Event { + + public CloseCallEvent() { + super("Close Call"); + } + + @Override + public void doEvent(HoleyMoleyGame game) { + for (Player player : game.getAlivePlayers()) { + player.setHealth(1); + player.addEffect(new Potion(PotionEffect.REGENERATION, 2, 4 * 20)); + } + } +} diff --git a/holeymoley/src/main/java/dev/emortal/minestom/holeymoley/event/Event.java b/holeymoley/src/main/java/dev/emortal/minestom/holeymoley/event/Event.java new file mode 100644 index 0000000..deba487 --- /dev/null +++ b/holeymoley/src/main/java/dev/emortal/minestom/holeymoley/event/Event.java @@ -0,0 +1,19 @@ +package dev.emortal.minestom.holeymoley.event; + +import dev.emortal.minestom.holeymoley.game.HoleyMoleyGame; + +public abstract class Event { + + private final String name; + + public Event(String name) { + this.name = name; + } + + public String getName() { + return name; + } + + public abstract void doEvent(HoleyMoleyGame game); + +} diff --git a/holeymoley/src/main/java/dev/emortal/minestom/holeymoley/event/EventManager.java b/holeymoley/src/main/java/dev/emortal/minestom/holeymoley/event/EventManager.java new file mode 100644 index 0000000..014fe0b --- /dev/null +++ b/holeymoley/src/main/java/dev/emortal/minestom/holeymoley/event/EventManager.java @@ -0,0 +1,30 @@ +package dev.emortal.minestom.holeymoley.event; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.concurrent.ThreadLocalRandom; + +public class EventManager { + + private final Map> eventMap = new HashMap<>(); + + public EventManager() {} + + public void registerEvent(String eventId, Class event) { + eventMap.put(eventId, event); + } + + public Event createRandomEvent() { + List> events = new ArrayList<>(eventMap.values()); + ThreadLocalRandom random = ThreadLocalRandom.current(); + Class randomEventClass = events.get(random.nextInt(events.size())); + try { + return randomEventClass.getDeclaredConstructor().newInstance(); + } catch (Exception exception) { + throw new RuntimeException(exception); + } + } + +} diff --git a/holeymoley/src/main/java/dev/emortal/minestom/holeymoley/event/HalfLifeEvent.java b/holeymoley/src/main/java/dev/emortal/minestom/holeymoley/event/HalfLifeEvent.java new file mode 100644 index 0000000..5a6c839 --- /dev/null +++ b/holeymoley/src/main/java/dev/emortal/minestom/holeymoley/event/HalfLifeEvent.java @@ -0,0 +1,21 @@ +package dev.emortal.minestom.holeymoley.event; + +import dev.emortal.minestom.holeymoley.game.HoleyMoleyGame; +import net.minestom.server.entity.Player; +import net.minestom.server.potion.Potion; +import net.minestom.server.potion.PotionEffect; + +public class HalfLifeEvent extends Event { + + public HalfLifeEvent() { + super("Half Life"); + } + + @Override + public void doEvent(HoleyMoleyGame game) { + for (Player player : game.getAlivePlayers()) { + player.setHealth(player.getHealth() / 2); + player.addEffect(new Potion(PotionEffect.REGENERATION, 1, 4 * 20)); + } + } +} diff --git a/holeymoley/src/main/java/dev/emortal/minestom/holeymoley/event/InventoryChaosEvent.java b/holeymoley/src/main/java/dev/emortal/minestom/holeymoley/event/InventoryChaosEvent.java new file mode 100644 index 0000000..77a7b24 --- /dev/null +++ b/holeymoley/src/main/java/dev/emortal/minestom/holeymoley/event/InventoryChaosEvent.java @@ -0,0 +1,29 @@ +package dev.emortal.minestom.holeymoley.event; + +import dev.emortal.minestom.holeymoley.game.HoleyMoleyGame; +import net.minestom.server.entity.Player; +import net.minestom.server.item.ItemStack; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +public class InventoryChaosEvent extends Event { + + public InventoryChaosEvent() { + super("Inventory Chaos"); + } + + @Override + public void doEvent(HoleyMoleyGame game) { + for (Player player : game.getAlivePlayers()) { + List items = new ArrayList<>(List.of(player.getInventory().getItemStacks())); + Collections.shuffle(items); + + int i = 0; + for (ItemStack item : items) { + player.getInventory().setItemStack(i++, item); + } + } + } +} diff --git a/holeymoley/src/main/java/dev/emortal/minestom/holeymoley/event/MinesweeperEvent.java b/holeymoley/src/main/java/dev/emortal/minestom/holeymoley/event/MinesweeperEvent.java new file mode 100644 index 0000000..0cad687 --- /dev/null +++ b/holeymoley/src/main/java/dev/emortal/minestom/holeymoley/event/MinesweeperEvent.java @@ -0,0 +1,37 @@ +package dev.emortal.minestom.holeymoley.event; + +import dev.emortal.minestom.holeymoley.game.HoleyMoleyGame; +import io.github.togar2.pvp.entity.explosion.TntEntity; +import net.minestom.server.ServerFlag; +import net.minestom.server.entity.Entity; +import net.minestom.server.event.EventListener; +import net.minestom.server.event.player.PlayerBlockBreakEvent; +import net.minestom.server.timer.TaskSchedule; + +import java.util.concurrent.ThreadLocalRandom; + +public class MinesweeperEvent extends Event { + + public MinesweeperEvent() { + super("Minesweeper"); + } + + @Override + public void doEvent(HoleyMoleyGame game) { + EventListener listener = EventListener.of(PlayerBlockBreakEvent.class, this::onBlockBroken); + game.getEventNode().addListener(listener); + + game.getInstance().scheduler() + .buildTask(() -> game.getEventNode().removeListener(listener)) + .delay(TaskSchedule.tick(ServerFlag.SERVER_TICKS_PER_SECOND * 10)) + .schedule(); + } + + private void onBlockBroken(PlayerBlockBreakEvent event) { + ThreadLocalRandom random = ThreadLocalRandom.current(); + if (random.nextDouble() >= 0.1) return; + + Entity entity = new TntEntity(null); + entity.setInstance(event.getInstance(), event.getBlockPosition().add(0.5, 0, 0.5)); + } +} diff --git a/holeymoley/src/main/java/dev/emortal/minestom/holeymoley/event/RainbowDirtEvent.java b/holeymoley/src/main/java/dev/emortal/minestom/holeymoley/event/RainbowDirtEvent.java new file mode 100644 index 0000000..0bc0db2 --- /dev/null +++ b/holeymoley/src/main/java/dev/emortal/minestom/holeymoley/event/RainbowDirtEvent.java @@ -0,0 +1,63 @@ +package dev.emortal.minestom.holeymoley.event; + +import dev.emortal.minestom.holeymoley.game.HoleyMoleyGame; +import net.minestom.server.coordinate.Point; +import net.minestom.server.coordinate.Pos; +import net.minestom.server.coordinate.Vec; +import net.minestom.server.entity.Player; +import net.minestom.server.instance.block.Block; + +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.ThreadLocalRandom; + +public class RainbowDirtEvent extends Event { + + private static final Block[] RAINBOW_BLOCKS = new Block[]{ + Block.RED_CONCRETE_POWDER, + Block.ORANGE_CONCRETE_POWDER, + Block.YELLOW_CONCRETE_POWDER, + Block.GREEN_CONCRETE_POWDER, + Block.BLUE_CONCRETE_POWDER, + Block.CYAN_CONCRETE_POWDER, + Block.LIGHT_BLUE_CONCRETE_POWDER, + Block.PURPLE_CONCRETE_POWDER, + Block.PINK_CONCRETE_POWDER, + Block.MAGENTA_CONCRETE_POWDER + }; + + public RainbowDirtEvent() { + super("Rainbow Dirt"); + } + + @Override + public void doEvent(HoleyMoleyGame game) { + List sphereBlocks = getBlocksInSphere(4); + ThreadLocalRandom random = ThreadLocalRandom.current(); + + for (Player player : game.getAlivePlayers()) { + for (Point sphereBlock : sphereBlocks) { + Pos pos = player.getPosition().add(sphereBlock); + + if (!game.getInstance().getBlock(pos, Block.Getter.Condition.TYPE).compare(Block.DIRT)) continue; + game.getInstance().setBlock(pos, RAINBOW_BLOCKS[random.nextInt(RAINBOW_BLOCKS.length)]); + } + } + } + + private List getBlocksInSphere(int radius) { + List points = new ArrayList<>(); + + for (int x = -radius; x < radius; x++) { + for (int y = -radius; y < radius; y++) { + for (int z = -radius; z < radius; z++) { + if (x * x + y * y + z * z > radius * radius) continue; + + points.add(new Vec(x, y, z)); + } + } + } + + return points; + } +} diff --git a/holeymoley/src/main/java/dev/emortal/minestom/holeymoley/event/TrapdoorEvent.java b/holeymoley/src/main/java/dev/emortal/minestom/holeymoley/event/TrapdoorEvent.java new file mode 100644 index 0000000..574c600 --- /dev/null +++ b/holeymoley/src/main/java/dev/emortal/minestom/holeymoley/event/TrapdoorEvent.java @@ -0,0 +1,24 @@ +package dev.emortal.minestom.holeymoley.event; + +import dev.emortal.minestom.holeymoley.game.HoleyMoleyGame; +import net.minestom.server.coordinate.Pos; +import net.minestom.server.entity.Player; +import net.minestom.server.instance.block.Block; + +public class TrapdoorEvent extends Event { + + public TrapdoorEvent() { + super("Trapdoor"); + } + + @Override + public void doEvent(HoleyMoleyGame game) { + for (Player player : game.getAlivePlayers()) { + for (int i = 0; i < 20; i++) { + Pos pos = player.getPosition().sub(0, i, 0); + if (game.getInstance().getBlock(pos, Block.Getter.Condition.TYPE).compare(Block.BEDROCK)) break; + game.getInstance().setBlock(pos, Block.AIR); + } + } + } +} diff --git a/holeymoley/src/main/java/dev/emortal/minestom/holeymoley/game/HoleyMoleyGame.java b/holeymoley/src/main/java/dev/emortal/minestom/holeymoley/game/HoleyMoleyGame.java new file mode 100644 index 0000000..09f53ff --- /dev/null +++ b/holeymoley/src/main/java/dev/emortal/minestom/holeymoley/game/HoleyMoleyGame.java @@ -0,0 +1,372 @@ +package dev.emortal.minestom.holeymoley.game; + +import dev.emortal.minestom.core.game.Game; +import dev.emortal.minestom.core.game.config.GameCreationInfo; +import dev.emortal.minestom.core.game.util.GameWinLoseMessages; +import dev.emortal.minestom.core.map.LoadedMap; +import dev.emortal.minestom.holeymoley.HoleyMoleyModule; +import dev.emortal.minestom.holeymoley.chest.ChestBlockHandler; +import dev.emortal.minestom.holeymoley.chest.ChestUpdateHandler; +import dev.emortal.minestom.holeymoley.event.BlindnessEvent; +import dev.emortal.minestom.holeymoley.event.CloseCallEvent; +import dev.emortal.minestom.holeymoley.event.EventManager; +import dev.emortal.minestom.holeymoley.event.HalfLifeEvent; +import dev.emortal.minestom.holeymoley.event.InventoryChaosEvent; +import dev.emortal.minestom.holeymoley.event.MinesweeperEvent; +import dev.emortal.minestom.holeymoley.event.RainbowDirtEvent; +import dev.emortal.minestom.holeymoley.event.TrapdoorEvent; +import dev.emortal.minestom.holeymoley.listeners.PvpListener; +import io.github.togar2.pvp.damage.combat.CombatManager; +import io.github.togar2.pvp.feature.CombatFeatures; +import io.github.togar2.pvp.feature.FeatureType; +import io.github.togar2.pvp.feature.cooldown.VanillaItemCooldownFeature; +import io.github.togar2.pvp.feature.explosion.ExplosionFeature; +import io.github.togar2.pvp.feature.food.VanillaExhaustionFeature; +import io.github.togar2.pvp.feature.food.VanillaRegenerationFeature; +import io.github.togar2.pvp.feature.tracking.VanillaDeathMessageFeature; +import net.kyori.adventure.key.Key; +import net.kyori.adventure.sound.Sound; +import net.kyori.adventure.text.Component; +import net.kyori.adventure.text.format.NamedTextColor; +import net.kyori.adventure.text.format.TextDecoration; +import net.kyori.adventure.text.minimessage.MiniMessage; +import net.kyori.adventure.title.Title; +import net.minestom.server.MinecraftServer; +import net.minestom.server.component.DataComponents; +import net.minestom.server.coordinate.Point; +import net.minestom.server.coordinate.Pos; +import net.minestom.server.coordinate.Vec; +import net.minestom.server.entity.EquipmentSlotGroup; +import net.minestom.server.entity.GameMode; +import net.minestom.server.entity.ItemEntity; +import net.minestom.server.entity.Player; +import net.minestom.server.entity.attribute.Attribute; +import net.minestom.server.entity.attribute.AttributeModifier; +import net.minestom.server.entity.attribute.AttributeOperation; +import net.minestom.server.event.item.ItemDropEvent; +import net.minestom.server.event.item.PickupItemEvent; +import net.minestom.server.event.player.PlayerBlockBreakEvent; +import net.minestom.server.event.player.PlayerBlockPlaceEvent; +import net.minestom.server.instance.Instance; +import net.minestom.server.instance.block.Block; +import net.minestom.server.inventory.TransactionOption; +import net.minestom.server.item.ItemStack; +import net.minestom.server.item.Material; +import net.minestom.server.item.component.AttributeList; +import net.minestom.server.item.component.EnchantmentList; +import net.minestom.server.item.enchant.Enchantment; +import net.minestom.server.sound.SoundEvent; +import net.minestom.server.timer.Task; +import net.minestom.server.timer.TaskSchedule; +import net.minestom.server.utils.time.TimeUnit; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import java.time.Duration; +import java.util.Collections; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Map; +import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ThreadLocalRandom; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.function.Supplier; + +public class HoleyMoleyGame extends Game { + private static final MiniMessage MINI_MESSAGE = MiniMessage.miniMessage(); + + private static final ItemStack EFFICIENCY_SHOVEL = ItemStack.builder(Material.IRON_SHOVEL) + .set(DataComponents.ENCHANTMENTS, new EnchantmentList(Map.of(Enchantment.EFFICIENCY, 5))) + .set(DataComponents.ATTRIBUTE_MODIFIERS, new AttributeList(new AttributeList.Modifier(Attribute.BLOCK_BREAK_SPEED, new AttributeModifier(Key.key("holeymoley"), 3, AttributeOperation.ADD_MULTIPLIED_TOTAL), EquipmentSlotGroup.HAND))) + .build(); + private static final ItemStack DIRT_STACK_ITEM = ItemStack.of(Material.DIRT, 64); + private static final ItemStack DIRT_ITEM = ItemStack.of(Material.DIRT); + + private final AtomicBoolean started = new AtomicBoolean(); + private final AtomicBoolean ended = new AtomicBoolean(); + + private final EventManager eventManager = new EventManager(); + + private @Nullable Task regularGlowTask = null; + private @Nullable Task regularEventTask = null; + + private final Set playerPlacedBlocks = new HashSet<>(); + + public HoleyMoleyGame(@NotNull GameCreationInfo creationInfo, @NotNull LoadedMap map) { + super(creationInfo, map); + + this.eventManager.registerEvent("blindness", BlindnessEvent.class); + this.eventManager.registerEvent("closecall", CloseCallEvent.class); + this.eventManager.registerEvent("halflife", HalfLifeEvent.class); + this.eventManager.registerEvent("inventorychaos", InventoryChaosEvent.class); + this.eventManager.registerEvent("minesweeper", MinesweeperEvent.class); + this.eventManager.registerEvent("rainbowdirt", RainbowDirtEvent.class); + this.eventManager.registerEvent("trapdoor", TrapdoorEvent.class); + } + + @Override + public void onPreJoin(@NotNull Player player) { + ThreadLocalRandom random = ThreadLocalRandom.current(); + int radius = HoleyMoleyModule.MAP_RADIUS; + + player.setRespawnPoint(new Pos( + random.nextInt(-radius + 1, radius) + 0.5, + random.nextInt(-radius + 1, radius - 1), // player is 2 blocks tall + random.nextInt(-radius + 1, radius) + 0.5 + )); + getInstance().setBlock(player.getRespawnPoint(), Block.AIR); + getInstance().setBlock(player.getRespawnPoint().add(0, 1, 0), Block.AIR); + } + + @Override + public void onJoin(@NotNull Player player) { + player.setAutoViewable(true); + player.setTeam(PlayerTeams.ALIVE); + player.setGlowing(false); + player.setGameMode(GameMode.ADVENTURE); + + player.getInventory().setItemStack(0, EFFICIENCY_SHOVEL); + } + + @Override + public void onLeave(@NotNull Player player) { + player.setTeam(null); + player.clearEffects(); + + this.checkPlayerCounts(); + } + + @Override + public void start() { + this.started.set(true); + + getInstance().scheduler().submitTask(new Supplier<>() { + int i = 5; + + @Override + public @NotNull TaskSchedule get() { + if (this.i == 3) { + playSound(Sound.sound(SoundEvent.BLOCK_PORTAL_TRIGGER, Sound.Source.MASTER, 0.45f, 1.27f)); + } + + if (this.i == 0) { + clearTitle(); + startGame(); + return TaskSchedule.stop(); + } + + showCountdown(this.i); + this.i--; + return TaskSchedule.seconds(1); + } + }); + } + + private void showCountdown(final int countdown) { + this.playSound(Sound.sound(Key.key("battle.countdown.begin"), Sound.Source.MASTER, 1F, 1F), Sound.Emitter.self()); + this.showTitle(Title.title( + Component.text(countdown, NamedTextColor.GREEN, TextDecoration.BOLD), + Component.empty(), + Title.Times.times(Duration.ZERO, Duration.ofMillis(1500), Duration.ofMillis(500)) + )); + } + + public void startGame() { + this.regularGlowTask = getInstance().scheduler().submitTask(new RegularGlowTask(this)); + this.regularEventTask = getInstance().scheduler().submitTask(new RegularEventTask(this)); + new PvpListener(this); + ChestUpdateHandler chestUpdateHandler = new ChestUpdateHandler(this); + + for (Player player : getPlayers()) { + // silly minestompvp + player.setTag(VanillaItemCooldownFeature.COOLDOWN_END, new HashMap<>()); + player.setTag(VanillaDeathMessageFeature.COMBAT_MANAGER, new CombatManager(player)); + player.setTag(VanillaExhaustionFeature.EXHAUSTION, 0.0f); + player.setTag(VanillaRegenerationFeature.STARVATION_TICKS, 0); + player.setGameMode(GameMode.SURVIVAL); + } + + getEventNode().addChild(CombatFeatures.modernVanilla().createNode()); // add minestompvp events + + getEventNode().addListener(ItemDropEvent.class, event -> { + ItemEntity itemEntity = new ItemEntity(event.getItemStack()); + itemEntity.setPickupDelay(40, TimeUnit.SERVER_TICK); + Vec velocity = event.getEntity().getPosition().direction().mul(6); + itemEntity.setVelocity(velocity); + itemEntity.setInstance(event.getEntity().getInstance(), event.getEntity().getPosition().add(0, event.getEntity().getEyeHeight(), 0)); + }); + + getEventNode().addListener(PickupItemEvent.class, event -> { + if (!(event.getLivingEntity() instanceof Player player)) return; + + boolean success = player.getInventory().addItemStack(event.getItemStack()); + event.setCancelled(!success); + }); + + getEventNode().addListener(PlayerBlockPlaceEvent.class, event -> { + if (event.getBlock().compare(Block.TNT)) { // tnt block should immediately prime + event.setCancelled(true); + ItemStack newUsedItem = event.getPlayer().getItemInHand(event.getHand()).consume(1); + event.getPlayer().setItemInHand(event.getHand(), newUsedItem); + + CombatFeatures.modernVanilla().get(FeatureType.EXPLOSION) + .primeExplosive(getInstance(), event.getBlockPosition(), new ExplosionFeature.IgnitionCause.ByPlayer(event.getPlayer()), 40); + return; + } + + playerPlacedBlocks.add(event.getBlockPosition()); + }); + + getEventNode().addListener(PlayerBlockBreakEvent.class, event -> { + Player player = event.getPlayer(); + Block block = event.getBlock(); + + boolean hasStackOfDirt = player.getInventory().takeItemStack(DIRT_STACK_ITEM, TransactionOption.DRY_RUN); + if (block.compare(Block.DIRT) && !hasStackOfDirt) { + player.playSound( + Sound.sound(SoundEvent.ENTITY_ITEM_PICKUP, Sound.Source.PLAYER, 0.25f, 1f), + Sound.Emitter.self() + ); + player.getInventory().addItemStack(DIRT_ITEM); + } + + if (block.compare(Block.CHEST)) { + ChestBlockHandler chestHandler = chestUpdateHandler.getOrCreateChestHandler(event.getBlockPosition()); + PvpListener.dropItems(chestHandler.getInventory(), getInstance(), event.getBlockPosition()); + } + + if (ThreadLocalRandom.current().nextDouble() < 0.005) { // 0.5% chance per block broken to spawn chest + if (playerPlacedBlocks.contains(event.getBlockPosition())) return; // Don't allow chests to spawn in player-placed blocks + + player.sendMessage( + Component.text() + .append(Component.text("★", NamedTextColor.GOLD)) + .append(Component.text(" | ", NamedTextColor.DARK_GRAY)) + .append(Component.text("You uncovered a ", NamedTextColor.GRAY)) + .append(Component.text("CHEST", NamedTextColor.LIGHT_PURPLE, TextDecoration.BOLD)) + .append(Component.text("!", NamedTextColor.GRAY)) + ); + + player.playSound( + Sound.sound(SoundEvent.ENTITY_PLAYER_LEVELUP, Sound.Source.PLAYER, 1f, 1.5f), + event.getBlockPosition() + ); + + event.setResultBlock(Block.CHEST); + chestUpdateHandler.getOrCreateChestHandler(event.getBlockPosition()); + } + }); + } + + public void checkPlayerCounts() { + Set alivePlayers = this.getAlivePlayers(); + + if (alivePlayers.isEmpty()) { + this.finish(); + return; + } + + if (alivePlayers.size() == 1) { + if (this.started.get()) { + this.victory(alivePlayers.iterator().next()); + } else { + this.finish(); + } + + return; + } + + // TODO: show remaining players somewhere + } + + public void victory(@Nullable Player winner) { + this.ended.set(true); + + // cancel ongoing tasks + if (this.regularGlowTask != null) this.regularGlowTask.cancel(); + if (this.regularEventTask != null) this.regularEventTask.cancel(); + + Title victoryTitle = Title.title( + MINI_MESSAGE.deserialize("VICTORY!"), + Component.text(GameWinLoseMessages.randomVictory(), NamedTextColor.GRAY), + Title.Times.times(Duration.ZERO, Duration.ofSeconds(3), Duration.ofSeconds(3)) + ); + Title defeatTitle = Title.title( + MINI_MESSAGE.deserialize("DEFEAT!"), + Component.text(GameWinLoseMessages.randomDefeat(), NamedTextColor.GRAY), + Title.Times.times(Duration.ZERO, Duration.ofSeconds(3), Duration.ofSeconds(3)) + ); + + Sound defeatSound = Sound.sound(SoundEvent.ENTITY_VILLAGER_NO, Sound.Source.MASTER, 1f, 1f); + Sound victorySound = Sound.sound(SoundEvent.BLOCK_BEACON_POWER_SELECT, Sound.Source.MASTER, 1f, 0.8f); + + // If there is no winner, choose the player with the highest kills + if (winner == null) { + winner = this.findPlayerWithHighestKills(); + } + + for (Player player : this.getPlayers()) { + player.setInvulnerable(true); + + if (winner == player) { + player.showTitle(victoryTitle); + player.playSound(victorySound, Sound.Emitter.self()); + } else { + player.showTitle(defeatTitle); + player.playSound(defeatSound, Sound.Emitter.self()); + } + } + + getInstance().scheduler().buildTask(this::finish) + .delay(TaskSchedule.seconds(6)) + .schedule(); + } + + private @Nullable Player findPlayerWithHighestKills() { + int killsRecord = 0; + Player highestKiller = null; + + for (Player player : this.getPlayers()) { + Integer playerKills = player.getTag(PvpListener.KILLS_TAG); + if (playerKills == null) playerKills = 0; + if (playerKills > killsRecord) { + killsRecord = playerKills; + highestKiller = player; + } + } + + return highestKiller; + } + + public EventManager getEventManager() { + return eventManager; + } + + @Override + public void cleanUp() { + getInstance().scheduleNextTick(MinecraftServer.getInstanceManager()::unregisterInstance); + } + + @Override + public @NotNull Instance getSpawningInstance(@NotNull Player player) { + return getInstance(); + } + + public @NotNull Instance getInstance() { + return getMap().instance(); + } + + public @NotNull Set getAlivePlayers() { + Set alivePlayers = ConcurrentHashMap.newKeySet(); + for (Player player : getPlayers()) { + if (player.getGameMode() == GameMode.SPECTATOR) continue; + alivePlayers.add(player); + } + return Collections.unmodifiableSet(alivePlayers); + } + + public boolean hasEnded() { + return this.ended.get(); + } +} diff --git a/holeymoley/src/main/java/dev/emortal/minestom/holeymoley/game/PlayerTeams.java b/holeymoley/src/main/java/dev/emortal/minestom/holeymoley/game/PlayerTeams.java new file mode 100644 index 0000000..4d53ab4 --- /dev/null +++ b/holeymoley/src/main/java/dev/emortal/minestom/holeymoley/game/PlayerTeams.java @@ -0,0 +1,26 @@ +package dev.emortal.minestom.holeymoley.game; + +import net.kyori.adventure.text.Component; +import net.kyori.adventure.text.format.NamedTextColor; +import net.minestom.server.MinecraftServer; +import net.minestom.server.color.TeamColor; +import net.minestom.server.network.packet.server.play.TeamsPacket; +import net.minestom.server.scoreboard.Team; +import org.jetbrains.annotations.NotNull; + +public final class PlayerTeams { + public static final @NotNull Team ALIVE = MinecraftServer.getTeamManager().createBuilder("holeymoley_alive") + .teamColor(TeamColor.RED) + .nameTagVisibility(TeamsPacket.NameTagVisibility.ALWAYS) + .updateTeamPacket() + .build(); + public static final @NotNull Team DEAD = MinecraftServer.getTeamManager().createBuilder("holeymoley_dead") + .teamColor(TeamColor.GRAY) + .prefix(Component.text("☠ ", NamedTextColor.GRAY)) + .nameTagVisibility(TeamsPacket.NameTagVisibility.NEVER) + .updateTeamPacket() + .build(); + + private PlayerTeams() { + } +} diff --git a/holeymoley/src/main/java/dev/emortal/minestom/holeymoley/game/RegularEventTask.java b/holeymoley/src/main/java/dev/emortal/minestom/holeymoley/game/RegularEventTask.java new file mode 100644 index 0000000..e8b2480 --- /dev/null +++ b/holeymoley/src/main/java/dev/emortal/minestom/holeymoley/game/RegularEventTask.java @@ -0,0 +1,64 @@ +package dev.emortal.minestom.holeymoley.game; + +import dev.emortal.minestom.holeymoley.event.Event; +import net.kyori.adventure.sound.Sound; +import net.kyori.adventure.text.Component; +import net.kyori.adventure.text.format.NamedTextColor; +import net.kyori.adventure.text.minimessage.MiniMessage; +import net.kyori.adventure.title.Title; +import net.minestom.server.ServerFlag; +import net.minestom.server.sound.SoundEvent; +import net.minestom.server.timer.TaskSchedule; +import org.jetbrains.annotations.NotNull; + +import java.time.Duration; +import java.util.function.Supplier; + +final class RegularEventTask implements Supplier { + private static final int EVENT_REPEAT = 40; + private static final int INITIAL_EVENT_DELAY = 30; + + private final @NotNull HoleyMoleyGame game; + + private boolean first = true; + + RegularEventTask(@NotNull HoleyMoleyGame game) { + this.game = game; + } + + @Override + public TaskSchedule get() { + if (first) { + first = false; + return TaskSchedule.tick(ServerFlag.SERVER_TICKS_PER_SECOND * INITIAL_EVENT_DELAY); + } + + startEvent(); + + return TaskSchedule.tick(ServerFlag.SERVER_TICKS_PER_SECOND * EVENT_REPEAT); + } + + private void startEvent() { + Event event = this.game.getEventManager().createRandomEvent(); + event.doEvent(this.game); + + this.game.playSound(Sound.sound(SoundEvent.ENTITY_ENDER_DRAGON_GROWL, Sound.Source.MASTER, 0.6f, 2f)); + + this.game.showTitle( + Title.title( + MiniMessage.miniMessage().deserialize("" + event.getName()), + Component.empty(), + Title.Times.times(Duration.ZERO, Duration.ofSeconds(3), Duration.ofSeconds(1)) + ) + ); + + this.game.sendMessage( + Component.text() + .append(Component.text("★", NamedTextColor.GOLD)) + .append(Component.text(" | ", NamedTextColor.DARK_GRAY)) + .append(Component.text("New event! ", NamedTextColor.GRAY)) + .append(Component.text(event.getName(), NamedTextColor.GOLD)) + ); + } + +} diff --git a/holeymoley/src/main/java/dev/emortal/minestom/holeymoley/game/RegularGlowTask.java b/holeymoley/src/main/java/dev/emortal/minestom/holeymoley/game/RegularGlowTask.java new file mode 100644 index 0000000..0e51e23 --- /dev/null +++ b/holeymoley/src/main/java/dev/emortal/minestom/holeymoley/game/RegularGlowTask.java @@ -0,0 +1,92 @@ +package dev.emortal.minestom.holeymoley.game; + +import net.kyori.adventure.sound.Sound; +import net.kyori.adventure.text.Component; +import net.kyori.adventure.text.format.NamedTextColor; +import net.minestom.server.ServerFlag; +import net.minestom.server.entity.Player; +import net.minestom.server.sound.SoundEvent; +import net.minestom.server.timer.TaskSchedule; +import org.jetbrains.annotations.NotNull; + +import java.util.function.Supplier; + +final class RegularGlowTask implements Supplier { + private static final int GLOW_REPEAT = 40; + private static final int INITIAL_GLOW_DELAY = 15; + private static final int WARN_GLOW = 3; + private static final int GLOW_DURATION = 4; + + private final @NotNull HoleyMoleyGame game; + + private boolean first = true; + private int warnSeconds = WARN_GLOW; + + RegularGlowTask(@NotNull HoleyMoleyGame game) { + this.game = game; + } + + @Override + public TaskSchedule get() { + if (first) { + first = false; + return TaskSchedule.tick(ServerFlag.SERVER_TICKS_PER_SECOND * INITIAL_GLOW_DELAY); + } + + if (warnSeconds > 0) { + if (warnSeconds == WARN_GLOW) { + this.game.playSound(Sound.sound(SoundEvent.BLOCK_NOTE_BLOCK_PLING, Sound.Source.MASTER, 1f, 0.5f)); + } else { + this.game.playSound(Sound.sound(SoundEvent.BLOCK_WOODEN_BUTTON_CLICK_OFF, Sound.Source.MASTER, 1f, 1.2f)); + } + sendWarnGlowingMessage(warnSeconds); + warnSeconds--; + return TaskSchedule.tick(ServerFlag.SERVER_TICKS_PER_SECOND); + } + + startGlowing(); + warnSeconds = WARN_GLOW; + + return TaskSchedule.tick(ServerFlag.SERVER_TICKS_PER_SECOND * GLOW_REPEAT); + } + + private void sendWarnGlowingMessage(int seconds) { + this.game.sendMessage( + Component.text() + .append(Component.text("⚠", NamedTextColor.RED)) + .append(Component.text(" | ", NamedTextColor.DARK_GRAY)) + .append(Component.text("Players glow in ", NamedTextColor.GRAY)) + .append(Component.text(seconds, NamedTextColor.RED)) + .append(Component.space()) + .append(Component.text(seconds == 1 ? "second" : "seconds", NamedTextColor.GRAY)) + ); + } + + private void startGlowing() { + this.game.sendMessage( + Component.text() + .append(Component.text("⚠", NamedTextColor.RED)) + .append(Component.text(" | ", NamedTextColor.DARK_GRAY)) + .append(Component.text("Players are now glowing!", NamedTextColor.RED)) + ); + + this.game.playSound(Sound.sound(SoundEvent.BLOCK_ANVIL_LAND, Sound.Source.MASTER, 0.5f, 1f)); + + for (Player alive : this.game.getAlivePlayers()) { + alive.setGlowing(true); + } + + this.game.getInstance().scheduler().buildTask(this::stopGlowing) + .delay(TaskSchedule.tick(ServerFlag.SERVER_TICKS_PER_SECOND * GLOW_DURATION)) + .schedule(); + } + + private void stopGlowing() { + this.game.playSound(Sound.sound(SoundEvent.BLOCK_WOODEN_BUTTON_CLICK_OFF, Sound.Source.MASTER, 1f, 1.2f)); + + for (Player alive : this.game.getAlivePlayers()) { + alive.setGlowing(false); + } + } + +} diff --git a/holeymoley/src/main/java/dev/emortal/minestom/holeymoley/listeners/PvpListener.java b/holeymoley/src/main/java/dev/emortal/minestom/holeymoley/listeners/PvpListener.java new file mode 100644 index 0000000..1df1389 --- /dev/null +++ b/holeymoley/src/main/java/dev/emortal/minestom/holeymoley/listeners/PvpListener.java @@ -0,0 +1,225 @@ +package dev.emortal.minestom.holeymoley.listeners; + +import dev.emortal.minestom.holeymoley.game.HoleyMoleyGame; +import dev.emortal.minestom.holeymoley.game.PlayerTeams; +import io.github.togar2.pvp.events.EntityPreDeathEvent; +import io.github.togar2.pvp.events.FinalDamageEvent; +import net.kyori.adventure.key.Key; +import net.kyori.adventure.sound.Sound; +import net.kyori.adventure.text.Component; +import net.kyori.adventure.text.TextComponent; +import net.kyori.adventure.text.format.NamedTextColor; +import net.kyori.adventure.text.format.TextDecoration; +import net.kyori.adventure.title.Title; +import net.minestom.server.ServerFlag; +import net.minestom.server.collision.BoundingBox; +import net.minestom.server.coordinate.Point; +import net.minestom.server.coordinate.Vec; +import net.minestom.server.entity.Entity; +import net.minestom.server.entity.GameMode; +import net.minestom.server.entity.ItemEntity; +import net.minestom.server.entity.Player; +import net.minestom.server.entity.damage.Damage; +import net.minestom.server.entity.damage.DamageType; +import net.minestom.server.event.entity.EntityDamageEvent; +import net.minestom.server.event.player.PlayerTickEvent; +import net.minestom.server.instance.Instance; +import net.minestom.server.instance.block.Block; +import net.minestom.server.inventory.AbstractInventory; +import net.minestom.server.item.ItemStack; +import net.minestom.server.network.packet.server.play.HitAnimationPacket; +import net.minestom.server.potion.PotionEffect; +import net.minestom.server.potion.TimedPotion; +import net.minestom.server.registry.RegistryKey; +import net.minestom.server.tag.Tag; +import net.minestom.server.utils.time.TimeUnit; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import java.time.Duration; +import java.util.concurrent.ThreadLocalRandom; + +public final class PvpListener { + public static final @NotNull Tag KILLS_TAG = Tag.Integer("kills"); + + private static final Title YOU_DIED_TITLE = Title.title( + Component.text("YOU DIED", NamedTextColor.RED, TextDecoration.BOLD), + Component.empty(), + Title.DEFAULT_TIMES + ); + + private final @NotNull HoleyMoleyGame game; + + public PvpListener(@NotNull HoleyMoleyGame game) { + this.game = game; + + game.getEventNode().addListener(FinalDamageEvent.class, this::onFinalDamage); + game.getEventNode().addListener(EntityPreDeathEvent.class, this::onPreDeath); + game.getEventNode().addListener(PlayerTickEvent.class, this::onTick); + game.getEventNode().addListener(EntityDamageEvent.class, this::onEntityDamage); + } + + public static void dropItems(@NotNull AbstractInventory inventory, @NotNull Instance instance, @NotNull Point point) { + ThreadLocalRandom random = ThreadLocalRandom.current(); + for (ItemStack itemStack : inventory.getItemStacks()) { + if (itemStack.isAir()) continue; + + double angle = random.nextDouble(Math.PI * 2); + Vec velocity = new Vec(random.nextDouble(3.0, 6.0), 0, 0) + .rotateAroundY(angle); + + ItemEntity itemEntity = new ItemEntity(itemStack); + itemEntity.setPickupDelay(20, TimeUnit.SERVER_TICK); + itemEntity.setVelocity(velocity); + itemEntity.setInstance(instance, point); + } + } + + private void onEntityDamage(@NotNull EntityDamageEvent event) { + if (!(event.getEntity() instanceof Player player)) return; + + if (event.getDamage().getType() == DamageType.FALL) { // https://github.com/TogAr2/MinestomPvP/pull/46#issuecomment-2309858332 + // if the player fell into water, cancel + Block blockAt = event.getInstance().getBlock(event.getEntity().getPosition(), Block.Getter.Condition.TYPE); + Block blockUnder = event.getInstance().getBlock(event.getEntity().getPosition().sub(0, 1.5, 0), Block.Getter.Condition.TYPE); + + if (blockAt.compare(Block.WATER) || blockUnder.compare(Block.WATER) || (blockUnder.compare(Block.SLIME_BLOCK) && !player.isSneaking())) { + event.setCancelled(true); + event.setAnimation(false); + event.getDamage().setAmount(0); + } + } + } + + private void onFinalDamage(@NotNull FinalDamageEvent event) { + if (event.isCancelled()) return; + if (!(event.getEntity() instanceof Player player)) return; + + if (player.isInvulnerable()) { + event.setCancelled(true); + return; + } + + Entity source = event.getEntity(); + + float animation = source.getPosition().yaw(); + this.sendHitAnimation(player, animation); + } + + private void sendHitAnimation(@NotNull Entity entity, float animation) { + this.game.sendGroupedPacket(new HitAnimationPacket(entity.getEntityId(), animation)); + } + + private void onPreDeath(@NotNull EntityPreDeathEvent event) { + if (!(event.getEntity() instanceof Player player)) return; + event.setCancelDeath(true); + + // Get last player that hit + Damage lastDamage = player.getLastDamageSource(); + // CustomEntityDamage should always be used over EntityDamage due to MinestomPvP + if (lastDamage != null && lastDamage.getSource() != null && lastDamage.getSource() instanceof Player killer) { + this.handlePlayerDeath(player, killer); + } else { + this.handlePlayerDeath(player, null); + } + + this.game.checkPlayerCounts(); + } + + private void handlePlayerDeath(@NotNull Player target, @Nullable Player killer) { + dropItems(target.getInventory(), target.getInstance(), target.getPosition().add(0, target.getEyeHeight(), 0)); + + this.game.playSound(Sound.sound(Key.key("battle.death"), Sound.Source.MASTER, 1f, 1f), Sound.Emitter.self()); + this.resetPlayerAfterDeath(target); + + this.sendDeathMessage(target, killer); + + if (killer != null) { + Integer lastKills = killer.getTag(KILLS_TAG); + if (lastKills == null) { + lastKills = 0; + } + killer.setTag(KILLS_TAG, lastKills + 1); + + killer.showTitle(Title.title( + Component.empty(), + Component.text("☠ " + target.getUsername(), NamedTextColor.RED), + Title.Times.times(Duration.ZERO, Duration.ofSeconds(1), Duration.ofSeconds(1)) + )); + + target.sendMessage( + Component.text() + .append(Component.text(killer.getUsername(), NamedTextColor.GOLD)) + .append(Component.text(" was on ", NamedTextColor.GRAY)) + .append(Component.text((int) killer.getHealth(), NamedTextColor.RED)) + .append(Component.text("/20", NamedTextColor.RED)) + ); + } + } + + private void resetPlayerAfterDeath(@NotNull Player player) { + player.setGameMode(GameMode.SPECTATOR); + player.setTeam(PlayerTeams.DEAD); + player.heal(); + player.setFood(20); + player.setAutoViewable(false); + player.setGlowing(false); + player.showTitle(YOU_DIED_TITLE); + player.getInventory().clear(); + } + + private void sendDeathMessage(@NotNull Player target, @Nullable Player killer) { + TextComponent.Builder result = Component.text() + .append(Component.text("☠", NamedTextColor.RED)) + .append(Component.text(" | ", NamedTextColor.DARK_GRAY)) + .append(Component.text(target.getUsername(), NamedTextColor.RED)); + + if (killer != null) { + result.append(Component.text(" was slain by ", NamedTextColor.GRAY)); + result.append(Component.text(killer.getUsername(), NamedTextColor.WHITE)); + } else { + result.append(Component.text(" died", NamedTextColor.GRAY)); + } + + this.game.sendMessage(result.build()); + } + + private void onTick(@NotNull PlayerTickEvent event) { + Player player = event.getPlayer(); + if (player.isInvulnerable()) return; + + BoundingBox.PointIterator blocksInHitbox = player.getBoundingBox().getBlocks(player.getPosition()); + + while (blocksInHitbox.hasNext()) { + BoundingBox.MutablePoint mutablePoint = blocksInHitbox.next(); + Block block = event.getInstance().getBlock(mutablePoint.blockX(), mutablePoint.blockY(), mutablePoint.blockZ(), Block.Getter.Condition.TYPE); + this.updateBurning(player, block); + } + } + + private void updateBurning(@NotNull Player player, @NotNull Block block) { + if (block.compare(Block.WATER)) { + player.setFireTicks(0); + } else if (block.compare(Block.FIRE)) { + this.doBurningDamage(player, 6, DamageType.IN_FIRE, 1F); + } else if (block.compare(Block.LAVA)) { + this.doBurningDamage(player, 12, DamageType.LAVA, 4F); + } + } + + private void doBurningDamage(@NotNull Player player, int durationSeconds, RegistryKey damageType, float damageAmount) { + player.setFireTicks(durationSeconds * ServerFlag.SERVER_TICKS_PER_SECOND); + if (player.getAliveTicks() % 10L != 0L) return; + + boolean hasFireResistance = false; + for (TimedPotion activeEffect : player.getActiveEffects()) { + if (activeEffect.potion().effect() == PotionEffect.FIRE_RESISTANCE) { + hasFireResistance = true; + break; + } + } + + if (hasFireResistance) return; + player.damage(damageType, damageAmount); + } +} diff --git a/holeymoley/src/main/java/dev/emortal/minestom/holeymoley/map/HoleyMoleyMapManager.java b/holeymoley/src/main/java/dev/emortal/minestom/holeymoley/map/HoleyMoleyMapManager.java new file mode 100644 index 0000000..494dbaa --- /dev/null +++ b/holeymoley/src/main/java/dev/emortal/minestom/holeymoley/map/HoleyMoleyMapManager.java @@ -0,0 +1,71 @@ +package dev.emortal.minestom.holeymoley.map; + +import dev.emortal.minestom.core.map.LoadedMap; +import dev.emortal.minestom.core.map.MapManager; +import io.github.togar2.pvp.feature.CombatFeatures; +import io.github.togar2.pvp.feature.FeatureType; +import net.kyori.adventure.key.Key; +import net.minestom.server.MinecraftServer; +import net.minestom.server.color.Color; +import net.minestom.server.instance.InstanceContainer; +import net.minestom.server.instance.block.Block; +import net.minestom.server.registry.RegistryKey; +import net.minestom.server.world.DimensionType; +import net.minestom.server.world.attribute.EnvironmentAttribute; +import org.jetbrains.annotations.NotNull; + +import java.util.Set; + +public final class HoleyMoleyMapManager implements MapManager { + + private final int radius; + private final RegistryKey dimension; + + public HoleyMoleyMapManager(int radius) { + this.radius = radius; + + DimensionType overworld = MinecraftServer.getDimensionTypeRegistry().get(DimensionType.OVERWORLD); + DimensionType dimensionType = DimensionType.builder() + .skylight(true) + .ambientLight(1f) + .setAttribute(EnvironmentAttribute.AMBIENT_LIGHT_COLOR, Color.WHITE) + .defaultClock(overworld.defaultClock()) + .timelines(overworld.timelines()) + .build(); + this.dimension = MinecraftServer.getDimensionTypeRegistry().register(Key.key("emortalmc:holeymoley"), dimensionType); + } + + @Override + public @NotNull LoadedMap loadMap(@NotNull String mapName) { + return loadRandomMap(); + } + + @Override + public @NotNull LoadedMap loadRandomMap() { + InstanceContainer instance = MinecraftServer.getInstanceManager().createInstanceContainer(this.dimension); + instance.setTime(0); + instance.defaultClock().pause(); + instance.setExplosionSupplier(CombatFeatures.modernVanilla().get(FeatureType.EXPLOSION).getExplosionSupplier()); + + // Generate a big cube of dirt surrounded by bedrock + int radius = this.radius; + instance.setGenerator(unit -> unit.modifier().setAll((x, y, z) -> { + if (y < -radius || y > radius) return Block.AIR; + if (x < -radius || x > radius) return Block.AIR; + if (z < -radius || z > radius) return Block.AIR; + + if (y == -radius || y == radius) return Block.BEDROCK; + if (x == -radius || x == radius) return Block.BEDROCK; + if (z == -radius || z == radius) return Block.BEDROCK; + + return Block.DIRT; + })); + + return new LoadedMap(instance, null); + } + + @Override + public @NotNull Set getMaps() { + return Set.of(); + } +} diff --git a/lobby/run/games.json b/lobby/run/games.json index c6a0a16..0416029 100644 --- a/lobby/run/games.json +++ b/lobby/run/games.json @@ -379,7 +379,7 @@ }, { "id": "holeymoley", - "enabled": false, + "enabled": true, "fleetName": "holeymoley", "priority": 120, "friendlyName": "Holey Moley", @@ -388,7 +388,7 @@ "maxPlayers": 20, "displayItem": { "material": "minecraft:iron_shovel", - "slot": 10, + "slot": 23, "name": "Holey Moley", "lore": [ "Fight to be the last", diff --git a/parkourtag/src/main/java/dev/emortal/minestom/parkourtag/ParkourTagGame.java b/parkourtag/src/main/java/dev/emortal/minestom/parkourtag/ParkourTagGame.java index 3f01086..17c170d 100755 --- a/parkourtag/src/main/java/dev/emortal/minestom/parkourtag/ParkourTagGame.java +++ b/parkourtag/src/main/java/dev/emortal/minestom/parkourtag/ParkourTagGame.java @@ -48,19 +48,19 @@ public class ParkourTagGame extends Game { private static final Pos SPAWN_POINT = new Pos(0.5, 65.0, 0.5); - public static final Team TAGGER_TEAM = MinecraftServer.getTeamManager().createBuilder("taggers") + public static final Team TAGGER_TEAM = MinecraftServer.getTeamManager().createBuilder("parkourtag_taggers") .teamColor(TeamColor.RED) .nameTagVisibility(TeamsPacket.NameTagVisibility.ALWAYS) .collisionRule(TeamsPacket.CollisionRule.NEVER) .updateTeamPacket() .build(); - public static final Team GOONS_TEAM = MinecraftServer.getTeamManager().createBuilder("goons") + public static final Team GOONS_TEAM = MinecraftServer.getTeamManager().createBuilder("parkourtag_goons") .teamColor(TeamColor.GREEN) .nameTagVisibility(TeamsPacket.NameTagVisibility.HIDE_FOR_OTHER_TEAMS) .collisionRule(TeamsPacket.CollisionRule.NEVER) .updateTeamPacket() .build(); - public static final Team DEAD_TEAM = MinecraftServer.getTeamManager().createBuilder("dead") + public static final Team DEAD_TEAM = MinecraftServer.getTeamManager().createBuilder("parkourtag_dead") .teamColor(TeamColor.GRAY) .prefix(Component.text("☠ ", NamedTextColor.GRAY)) .nameTagVisibility(TeamsPacket.NameTagVisibility.NEVER) diff --git a/settings.gradle.kts b/settings.gradle.kts index 936f146..64a65f5 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -11,6 +11,7 @@ include("lazertag") include("marathon") include("battle") include("minesweeper") +include("holeymoley") include("parkourtag") include("proxy") include("messaging")